commit a6dea9c8886717a4e33f59c59c67fc401627014e Author: Orlando M Guerreiro Date: Thu May 22 19:23:40 2025 +0100 Initial Project Commit diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..a914fd2 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/java/.devcontainer/base.Dockerfile + +# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 17, 17-bullseye, 17-buster +ARG VARIANT="17" +FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT} + +# [Option] Install Maven +ARG INSTALL_MAVEN="false" +ARG MAVEN_VERSION="" +# [Option] Install Gradle +ARG INSTALL_GRADLE="false" +ARG GRADLE_VERSION="" +RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ + && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f57c53a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,53 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/java +{ + "name": "Resilient", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a Java version: 17, 19 + // Append -bullseye or -buster to pin to an OS version. + // Use the -bullseye variants on local arm64/Apple Silicon. + "VARIANT": "17-bullseye", + // Options + // maven and gradle wrappers are used by default, we don't need them installed globally + // "INSTALL_MAVEN": "true", + // "INSTALL_GRADLE": "false", + "NODE_VERSION": "20.14.0" + } + }, + + "customizations": { + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "java.jdt.ls.java.home": "/docker-java-home" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "angular.ng-template", + "christian-kohler.npm-intellisense", + "firsttris.vscode-jest-runner", + "ms-vscode.vscode-typescript-tslint-plugin", + "dbaeumer.vscode-eslint", + "vscjava.vscode-java-pack", + "pivotal.vscode-boot-dev-pack", + "esbenp.prettier-vscode" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [4200, 3001, 9000, 8081], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "docker-in-docker": "latest", + "docker-from-docker": "latest" + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2fa6a2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +[*.{ts,tsx,js,jsx,json,css,scss,yml,html,vue}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..4997c23 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +node_modules/ +src/main/docker/ +jest.conf.js +webpack/ +target/ +build/ +node/ +coverage/ +postcss.config.js +target/classes/static/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..96996ae --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,99 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@angular-eslint/recommended", + "prettier", + "eslint-config-prettier" + ], + "env": { + "browser": true, + "es6": true, + "commonjs": true + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "project": ["./tsconfig.app.json", "./tsconfig.spec.json"] + }, + "rules": { + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "jhi", + "style": "kebab-case" + } + ], + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "jhi", + "style": "camelCase" + } + ], + "@angular-eslint/relative-url-prefix": "error", + "@typescript-eslint/ban-types": [ + "error", + { + "extendDefaults": true, + "types": { + "{}": false + } + } + ], + "@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }], + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/member-ordering": [ + "error", + { + "default": [ + "public-static-field", + "protected-static-field", + "private-static-field", + "public-instance-field", + "protected-instance-field", + "private-instance-field", + "constructor", + "public-static-method", + "protected-static-method", + "private-static-method", + "public-instance-method", + "protected-instance-method", + "private-instance-method" + ] + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-shadow": ["error"], + "@typescript-eslint/no-unnecessary-condition": "error", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/prefer-nullish-coalescing": "error", + "@typescript-eslint/prefer-optional-chain": "error", + "@typescript-eslint/unbound-method": "off", + "arrow-body-style": "error", + "curly": "error", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "guard-for-in": "error", + "no-bitwise": "error", + "no-caller": "error", + "no-console": ["error", { "allow": ["warn", "error"] }], + "no-eval": "error", + "no-labels": "error", + "no-new": "error", + "no-new-wrappers": "error", + "object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": true }], + "radix": "error", + "spaced-comment": ["warn", "always"] + } +} diff --git a/.factorypath b/.factorypath new file mode 100644 index 0000000..20c4556 --- /dev/null +++ b/.factorypath @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c90269 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Logs +*.log +logs/ +**/logs/ + +# OS files +.DS_Store +Thumbs.db + +# IDEs +.idea/ +.vscode/ +*.iml + +# Build & dependencies (Java) +target/ + +# Java cache +.classpath +.project +.settings/ +.gradle/ +build/ + +# Angular +node_modules/ +dist/ +.angular/ +.output/ +.env +.env.* + +# Angular cache +.turbo/ +.npm/ +.cache/ + +# Docker +*.pid +docker-compose.override.yml + +# Misc +*.bak +*.swp +*.tmp diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..adefefb --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + + +"$(dirname "$0")/../npmw" exec --no-install lint-staged diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 0000000..0531990 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,3 @@ +module.exports = { + '{,**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}': ['prettier --write'], +}; diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1 @@ + diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..f3ad4ba --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7d1ec5e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +node_modules +package-lock.json +.git +target/ + +# Generated by jhipster:maven +target +.mvn + +# Generated by jhipster:client +target/classes/static/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8550cbf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,22 @@ +# Prettier configuration + +printWidth: 140 +singleQuote: true +tabWidth: 2 +useTabs: false + +# js and ts rules: +arrowParens: avoid + +# jsx and tsx rules: +bracketSameLine: false + +plugins: + - prettier-plugin-packagejson + - prettier-plugin-java + +# java rules: +overrides: + - files: "*.java" + options: + tabWidth: 4 diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..7abc4df --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,43 @@ +{ + "generator-jhipster": { + "applicationType": "monolith", + "authenticationType": "session", + "baseName": "resilient", + "buildTool": "maven", + "cacheProvider": "ehcache", + "clientFramework": "angular", + "devDatabaseType": "mysql", + "devServerPort": 4200, + "enableHibernateCache": true, + "enableTranslation": true, + "entities": [ + "OrganizationType", + "Organization", + "MetadataProperty", + "MetadataValue", + "UnitType", + "Unit", + "UnitConverter", + "VariableScope", + "VariableCategory", + "Variable", + "VariableUnits", + "VariableClass", + "Period", + "PeriodVersion", + "InputData", + "InputDataUpload", + "InputDataUploadLog", + "OutputData" + ], + "jhipsterVersion": "8.5.0", + "languages": ["pt-pt", "en"], + "lastLiquibaseTimestamp": 1728984607000, + "nativeLanguage": "pt-pt", + "packageFolder": "com/oguerreiro/resilient", + "packageName": "com.oguerreiro.resilient", + "prodDatabaseType": "mysql", + "rememberMeKey": "6436150a69ff50bcf383fbb9d974e2e7bd5c4439beaeef76e87a042d920db55f1f161147c30e01db9fd82117e47db521be8f", + "serverPort": "8081" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3de15bd --- /dev/null +++ b/README.md @@ -0,0 +1,260 @@ +# resilient + +This application was generated using JHipster 8.5.0, you can find documentation and help at [https://www.jhipster.tech/documentation-archive/v8.5.0](https://www.jhipster.tech/documentation-archive/v8.5.0). + +## Project Structure + +Node is required for generation and recommended for development. `package.json` is always generated for a better development experience with prettier, commit hooks, scripts and so on. + +In the project root, JHipster generates configuration files for tools like git, prettier, eslint, husky, and others that are well known and you can find references in the web. + +`/src/*` structure follows default Java structure. + +- `.yo-rc.json` - Yeoman configuration file + JHipster configuration is stored in this file at `generator-jhipster` key. You may find `generator-jhipster-*` for specific blueprints configuration. +- `.yo-resolve` (optional) - Yeoman conflict resolver + Allows to use a specific action when conflicts are found skipping prompts for files that matches a pattern. Each line should match `[pattern] [action]` with pattern been a [Minimatch](https://github.com/isaacs/minimatch#minimatch) pattern and action been one of skip (default if omitted) or force. Lines starting with `#` are considered comments and are ignored. +- `.jhipster/*.json` - JHipster entity configuration files + +- `npmw` - wrapper to use locally installed npm. + JHipster installs Node and npm locally using the build tool by default. This wrapper makes sure npm is installed locally and uses it avoiding some differences different versions can cause. By using `./npmw` instead of the traditional `npm` you can configure a Node-less environment to develop or test your application. +- `/src/main/docker` - Docker configurations for the application and services that the application depends on + +## Development + +Before you can build this project, you must install and configure the following dependencies on your machine: + +1. [Node.js](https://nodejs.org/): We use Node to run a development web server and build the project. + Depending on your system, you can install Node either from source or as a pre-packaged bundle. + +After installing Node, you should be able to run the following command to install development tools. +You will only need to run this command when dependencies change in [package.json](package.json). + +``` +npm install +``` + +We use npm scripts and [Angular CLI][] with [Webpack][] as our build system. + +Run the following commands in two separate terminals to create a blissful development experience where your browser +auto-refreshes when files change on your hard drive. + +``` +./mvnw +npm start +``` + +Npm is also used to manage CSS and JavaScript dependencies used in this application. You can upgrade dependencies by +specifying a newer version in [package.json](package.json). You can also run `npm update` and `npm install` to manage dependencies. +Add the `help` flag on any command to see how you can use it. For example, `npm help update`. + +The `npm run` command will list all of the scripts available to run for this project. + +### PWA Support + +JHipster ships with PWA (Progressive Web App) support, and it's turned off by default. One of the main components of a PWA is a service worker. + +The service worker initialization code is disabled by default. To enable it, uncomment the following code in `src/main/webapp/app/app.config.ts`: + +```typescript +ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }), +``` + +### Managing dependencies + +For example, to add [Leaflet][] library as a runtime dependency of your application, you would run following command: + +``` +npm install --save --save-exact leaflet +``` + +To benefit from TypeScript type definitions from [DefinitelyTyped][] repository in development, you would run following command: + +``` +npm install --save-dev --save-exact @types/leaflet +``` + +Then you would import the JS and CSS files specified in library's installation instructions so that [Webpack][] knows about them: +Edit [src/main/webapp/app/app.config.ts](src/main/webapp/app/app.config.ts) file: + +``` +import 'leaflet/dist/leaflet.js'; +``` + +Edit [src/main/webapp/content/scss/vendor.scss](src/main/webapp/content/scss/vendor.scss) file: + +``` +@import 'leaflet/dist/leaflet.css'; +``` + +Note: There are still a few other things remaining to do for Leaflet that we won't detail here. + +For further instructions on how to develop with JHipster, have a look at [Using JHipster in development][]. + +### Using Angular CLI + +You can also use [Angular CLI][] to generate some custom client code. + +For example, the following command: + +``` +ng generate component my-component +``` + +will generate few files: + +``` +create src/main/webapp/app/my-component/my-component.component.html +create src/main/webapp/app/my-component/my-component.component.ts +update src/main/webapp/app/app.config.ts +``` + +## Building for production + +### Packaging as jar + +To build the final jar and optimize the resilient application for production, run: + +``` +./mvnw -Pprod clean verify +``` + +This will concatenate and minify the client CSS and JavaScript files. It will also modify `index.html` so it references these new files. +To ensure everything worked, run: + +``` +java -jar target/*.jar +``` + +Then navigate to [http://localhost:8081](http://localhost:8081) in your browser. + +Refer to [Using JHipster in production][] for more details. + +### Packaging as war + +To package your application as a war in order to deploy it to an application server, run: + +``` +./mvnw -Pprod,war clean verify +``` + +### JHipster Control Center + +JHipster Control Center can help you manage and control your application(s). You can start a local control center server (accessible on http://localhost:7419) with: + +``` +docker compose -f src/main/docker/jhipster-control-center.yml up +``` + +## Testing + +### Spring Boot tests + +To launch your application's tests, run: + +``` +./mvnw verify +``` + +### Client tests + +Unit tests are run by [Jest][]. They're located in [src/test/javascript/](src/test/javascript/) and can be run with: + +``` +npm test +``` + +## Others + +### Code quality using Sonar + +Sonar is used to analyse code quality. You can start a local Sonar server (accessible on http://localhost:9001) with: + +``` +docker compose -f src/main/docker/sonar.yml up -d +``` + +Note: we have turned off forced authentication redirect for UI in [src/main/docker/sonar.yml](src/main/docker/sonar.yml) for out of the box experience while trying out SonarQube, for real use cases turn it back on. + +You can run a Sonar analysis with using the [sonar-scanner](https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner) or by using the maven plugin. + +Then, run a Sonar analysis: + +``` +./mvnw -Pprod clean verify sonar:sonar -Dsonar.login=admin -Dsonar.password=admin +``` + +If you need to re-run the Sonar phase, please be sure to specify at least the `initialize` phase since Sonar properties are loaded from the sonar-project.properties file. + +``` +./mvnw initialize sonar:sonar -Dsonar.login=admin -Dsonar.password=admin +``` + +Additionally, Instead of passing `sonar.password` and `sonar.login` as CLI arguments, these parameters can be configured from [sonar-project.properties](sonar-project.properties) as shown below: + +``` +sonar.login=admin +sonar.password=admin +``` + +For more information, refer to the [Code quality page][]. + +### Using Docker to simplify development (optional) + +You can use Docker to improve your JHipster development experience. A number of docker-compose configuration are available in the [src/main/docker](src/main/docker) folder to launch required third party services. + +For example, to start a mysql database in a docker container, run: + +``` +docker compose -f src/main/docker/mysql.yml up -d +``` + +To stop it and remove the container, run: + +``` +docker compose -f src/main/docker/mysql.yml down +``` + +You can also fully dockerize your application and all the services that it depends on. +To achieve this, first build a docker image of your app by running: + +``` +npm run java:docker +``` + +Or build a arm64 docker image when using an arm64 processor os like MacOS with M1 processor family running: + +``` +npm run java:docker:arm64 +``` + +Then run: + +``` +docker compose -f src/main/docker/app.yml up -d +``` + +When running Docker Desktop on MacOS Big Sur or later, consider enabling experimental `Use the new Virtualization framework` for better processing performance ([disk access performance is worse](https://github.com/docker/roadmap/issues/7)). + +For more information refer to [Using Docker and Docker-Compose][], this page also contains information on the docker-compose sub-generator (`jhipster docker-compose`), which is able to generate docker configurations for one or several JHipster applications. + +## Continuous Integration (optional) + +To configure CI for your project, run the ci-cd sub-generator (`jhipster ci-cd`), this will let you generate configuration files for a number of Continuous Integration systems. Consult the [Setting up Continuous Integration][] page for more information. + +[JHipster Homepage and latest documentation]: https://www.jhipster.tech +[JHipster 8.5.0 archive]: https://www.jhipster.tech/documentation-archive/v8.5.0 +[Using JHipster in development]: https://www.jhipster.tech/documentation-archive/v8.5.0/development/ +[Using Docker and Docker-Compose]: https://www.jhipster.tech/documentation-archive/v8.5.0/docker-compose +[Using JHipster in production]: https://www.jhipster.tech/documentation-archive/v8.5.0/production/ +[Running tests page]: https://www.jhipster.tech/documentation-archive/v8.5.0/running-tests/ +[Code quality page]: https://www.jhipster.tech/documentation-archive/v8.5.0/code-quality/ +[Setting up Continuous Integration]: https://www.jhipster.tech/documentation-archive/v8.5.0/setting-up-ci/ +[Node.js]: https://nodejs.org/ +[NPM]: https://www.npmjs.com/ +[Webpack]: https://webpack.github.io/ +[BrowserSync]: https://www.browsersync.io/ +[Jest]: https://facebook.github.io/jest/ +[Leaflet]: https://leafletjs.com/ +[DefinitelyTyped]: https://definitelytyped.org/ +[Angular CLI]: https://cli.angular.io/ diff --git a/angular.json b/angular.json new file mode 100644 index 0000000..2b28d0d --- /dev/null +++ b/angular.json @@ -0,0 +1,118 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "resilient": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src/main/webapp", + "prefix": "jhi", + "architect": { + "build": { + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "customWebpackConfig": { + "path": "./webpack/webpack.custom.js" + }, + "outputPath": "target/classes/static/", + "index": "src/main/webapp/index.html", + "main": "src/main/webapp/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/main/webapp/content", + "src/main/webapp/favicon.ico", + "src/main/webapp/favicon.png", + "src/main/webapp/manifest.webapp", + "src/main/webapp/robots.txt" + ], + "styles": [ + "src/main/webapp/content/scss/vendor.scss", + "src/main/webapp/content/scss/global.scss", + "node_modules/@angular/material/prebuilt-themes/indigo-pink.css" + ], + "scripts": [ + "src/main/webapp/content/tinymce/tinymce.min.js" + ] + }, + "configurations": { + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "serviceWorker": true, + "ngswConfigPath": "ngsw-config.json", + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-builders/custom-webpack:dev-server", + "options": { + "buildTarget": "resilient:build:development", + "host": "0.0.0.0", + "port": 4200 + }, + "configurations": { + "production": { + "buildTarget": "resilient:build:production" + }, + "development": { + "buildTarget": "resilient:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular-builders/jest:run", + "options": { + "configPath": "jest.conf.js", + "tsConfig": "tsconfig.spec.json" + } + } + } + } + }, + "cli": { + "cache": { + "enabled": true, + "path": "./target/angular/", + "environment": "all" + }, + "packageManager": "npm" + } +} diff --git a/angular.json.old b/angular.json.old new file mode 100644 index 0000000..c503bcd --- /dev/null +++ b/angular.json.old @@ -0,0 +1,110 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "resilient": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src/main/webapp", + "prefix": "jhi", + "architect": { + "build": { + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "customWebpackConfig": { + "path": "./webpack/webpack.custom.js" + }, + "outputPath": "target/classes/static/", + "index": "src/main/webapp/index.html", + "main": "src/main/webapp/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/main/webapp/content", + "src/main/webapp/favicon.ico", + "src/main/webapp/manifest.webapp", + "src/main/webapp/robots.txt" + ], + "styles": ["src/main/webapp/content/scss/vendor.scss", "src/main/webapp/content/scss/global.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "serviceWorker": true, + "ngswConfigPath": "ngsw-config.json", + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-builders/custom-webpack:dev-server", + "options": { + "buildTarget": "resilient:build:development", + "port": 4200 + }, + "configurations": { + "production": { + "buildTarget": "resilient:build:production" + }, + "development": { + "buildTarget": "resilient:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular-builders/jest:run", + "options": { + "configPath": "jest.conf.js", + "tsConfig": "tsconfig.spec.json" + } + } + } + } + }, + "cli": { + "cache": { + "enabled": true, + "path": "./target/angular/", + "environment": "all" + }, + "packageManager": "npm" + } +} diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..5d5ae65 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/jest.conf.js b/jest.conf.js new file mode 100644 index 0000000..969b41f --- /dev/null +++ b/jest.conf.js @@ -0,0 +1,29 @@ +const { pathsToModuleNameMapper } = require('ts-jest'); + +const { + compilerOptions: { paths = {}, baseUrl = './' }, +} = require('./tsconfig.json'); +const environment = require('./webpack/environment'); + +module.exports = { + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$|dayjs/esm)'], + resolver: 'jest-preset-angular/build/resolvers/ng-jest-resolver.js', + globals: { + ...environment, + }, + roots: ['', `/${baseUrl}`], + modulePaths: [`/${baseUrl}`], + setupFiles: ['jest-date-mock'], + cacheDirectory: '/target/jest-cache', + coverageDirectory: '/target/test-results/', + moduleNameMapper: pathsToModuleNameMapper(paths, { prefix: `/${baseUrl}/` }), + reporters: [ + 'default', + ['jest-junit', { outputDirectory: '/target/test-results/', outputName: 'TESTS-results-jest.xml' }], + ['jest-sonar', { outputDirectory: './target/test-results/jest', outputName: 'TESTS-results-sonar.xml' }], + ], + testMatch: ['/src/main/webapp/app/**/@(*.)@(spec.ts)'], + testEnvironmentOptions: { + url: 'https://jhipster.tech', + }, +}; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..ac8e247 --- /dev/null +++ b/mvnw @@ -0,0 +1,250 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.1 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl="${value-}" ;; + distributionSha256Sum) distributionSha256Sum="${value-}" ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..7b0c094 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,146 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.1 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/ngsw-config.json b/ngsw-config.json new file mode 100644 index 0000000..8d57602 --- /dev/null +++ b/ngsw-config.json @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": ["/favicon.ico", "/index.html", "/manifest.webapp", "/*.css", "/*.js"] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": ["/content/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"] + } + } + ] +} diff --git a/npmw b/npmw new file mode 100644 index 0000000..785f188 --- /dev/null +++ b/npmw @@ -0,0 +1,42 @@ +#!/bin/sh + +basedir=`dirname "$0"` + +if [ -f "$basedir/mvnw" ]; then + bindir="$basedir/target/node" + repodir="$basedir/target/node/node_modules" + installCommand="$basedir/mvnw --batch-mode -ntp -Pwebapp frontend:install-node-and-npm@install-node-and-npm" + + PATH="$basedir/$builddir/:$PATH" + NPM_EXE="$basedir/$builddir/node_modules/npm/bin/npm-cli.js" + NODE_EXE="$basedir/$builddir/node" +elif [ -f "$basedir/gradlew" ]; then + bindir="$basedir/build/node/bin" + repodir="$basedir/build/node/lib/node_modules" + installCommand="$basedir/gradlew npmSetup" +else + echo "Using npm installed globally" + exec npm "$@" +fi + +NPM_EXE="$repodir/npm/bin/npm-cli.js" +NODE_EXE="$bindir/node" + +if [ ! -x "$NPM_EXE" ] || [ ! -x "$NODE_EXE" ]; then + $installCommand || true +fi + +if [ -x "$NODE_EXE" ]; then + echo "Using node installed locally $($NODE_EXE --version)" + PATH="$bindir:$PATH" +else + NODE_EXE='node' +fi + +if [ ! -x "$NPM_EXE" ]; then + echo "Local npm not found, using npm installed globally" + npm "$@" +else + echo "Using npm installed locally $($NODE_EXE $NPM_EXE --version)" + $NODE_EXE $NPM_EXE "$@" +fi diff --git a/npmw.cmd b/npmw.cmd new file mode 100644 index 0000000..b6e7980 --- /dev/null +++ b/npmw.cmd @@ -0,0 +1,31 @@ +@echo off + +setlocal + +set NPMW_DIR=%~dp0 + +if exist "%NPMW_DIR%mvnw.cmd" ( + set NODE_EXE=^"^" + set NODE_PATH=%NPMW_DIR%target\node\ + set NPM_EXE=^"%NPMW_DIR%target\node\npm.cmd^" + set INSTALL_NPM_COMMAND=^"%NPMW_DIR%mvnw.cmd^" -Pwebapp frontend:install-node-and-npm@install-node-and-npm +) else ( + set NODE_EXE=^"%NPMW_DIR%build\node\bin\node.exe^" + set NODE_PATH=%NPMW_DIR%build\node\bin\ + set NPM_EXE=^"%NPMW_DIR%build\node\lib\node_modules\npm\bin\npm-cli.js^" + set INSTALL_NPM_COMMAND=^"%NPMW_DIR%gradlew.bat^" npmSetup +) + +if not exist %NPM_EXE% ( + call %INSTALL_NPM_COMMAND% +) + +if exist %NODE_EXE% ( + Rem execute local npm with local node, whilst adding local node location to the PATH for this CMD session + endlocal & echo "%PATH%"|find /i "%NODE_PATH%;">nul || set "PATH=%NODE_PATH%;%PATH%" & call %NODE_EXE% %NPM_EXE% %* +) else if exist %NPM_EXE% ( + Rem execute local npm, whilst adding local npm location to the PATH for this CMD session + endlocal & echo "%PATH%"|find /i "%NODE_PATH%;">nul || set "PATH=%NODE_PATH%;%PATH%" & call %NPM_EXE% %* +) else ( + call npm %* +) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..71956d2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,28152 @@ +{ + "name": "resilient", + "version": "0.0.1-SNAPSHOT", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "resilient", + "version": "0.0.1-SNAPSHOT", + "license": "UNLICENSED", + "dependencies": { + "@angular/animations": "17.3.9", + "@angular/common": "17.3.9", + "@angular/compiler": "17.3.9", + "@angular/core": "17.3.9", + "@angular/forms": "17.3.9", + "@angular/localize": "17.3.9", + "@angular/material": "17.3.9", + "@angular/platform-browser": "17.3.9", + "@angular/platform-browser-dynamic": "17.3.9", + "@angular/router": "17.3.9", + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/lang-java": "^6.0.1", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.34.3", + "@fortawesome/angular-fontawesome": "0.14.1", + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@ng-bootstrap/ng-bootstrap": "16.0.0", + "@ng-select/ng-select": "^12.0.0", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", + "@popperjs/core": "2.11.8", + "@tinymce/tinymce-angular": "^8.0.1", + "@types/codemirror": "^5.60.15", + "bootstrap": "5.3.3", + "chart.js": "^3.9.1", + "codemirror": "^6.0.1", + "dayjs": "1.11.11", + "ng2-charts": "^3.1.2", + "ngx-color-picker": "^17.0.0", + "ngx-infinite-scroll": "17.0.0", + "quill-better-table": "^1.2.10", + "rxjs": "7.8.1", + "tslib": "2.6.2", + "zone.js": "0.14.6" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "17.0.2", + "@angular-builders/jest": "17.0.3", + "@angular-devkit/build-angular": "17.3.7", + "@angular-eslint/eslint-plugin": "17.5.2", + "@angular/cli": "17.3.7", + "@angular/compiler-cli": "17.3.9", + "@angular/service-worker": "17.3.9", + "@types/jest": "29.5.12", + "@types/node": "20.11.25", + "@types/quill": "^2.0.14", + "@typescript-eslint/eslint-plugin": "7.11.0", + "@typescript-eslint/parser": "7.11.0", + "browser-sync": "3.0.2", + "browser-sync-webpack-plugin": "2.3.0", + "buffer": "6.0.3", + "concurrently": "8.2.2", + "copy-webpack-plugin": "12.0.2", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-webpack-plugin": "4.2.0", + "folder-hash": "4.0.4", + "generator-jhipster": "8.5.0", + "husky": "9.0.11", + "jest": "29.7.0", + "jest-date-mock": "1.0.10", + "jest-environment-jsdom": "29.7.0", + "jest-junit": "16.0.0", + "jest-preset-angular": "14.1.0", + "jest-sonar": "0.2.16", + "lint-staged": "15.2.5", + "merge-jsons-webpack-plugin": "2.0.1", + "prettier": "3.2.5", + "prettier-plugin-java": "2.6.0", + "prettier-plugin-packagejson": "2.5.0", + "rimraf": "5.0.7", + "swagger-ui-dist": "5.17.14", + "ts-jest": "29.1.4", + "typescript": "5.4.5", + "wait-on": "7.2.0", + "webpack-bundle-analyzer": "4.10.2", + "webpack-merge": "5.10.0", + "webpack-notifier": "1.15.0" + }, + "engines": { + "node": ">=20.14.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-builders/common": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-1.0.2.tgz", + "integrity": "sha512-lUusRq6jN1It5LcUTLS6Q+AYAYGTo/EEN8hV0M6Ek9qXzweAouJaSEnwv7p04/pD7yJTl0YOCbN79u+wGm3x4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "^17.1.0", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + } + }, + "node_modules/@angular-builders/custom-webpack": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-17.0.2.tgz", + "integrity": "sha512-K0jqdW5UdVIeKiZXO4nLiiiVt0g6PKJELdxgjsBGMtyRk+RLEY+pIp1061oy/Yf09nGYseZ7Mdx3XASYHQjNwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-builders/common": "1.0.2", + "@angular-devkit/architect": ">=0.1700.0 < 0.1800.0", + "@angular-devkit/build-angular": "^17.0.0", + "@angular-devkit/core": "^17.0.0", + "lodash": "^4.17.15", + "webpack-merge": "^5.7.3" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0" + } + }, + "node_modules/@angular-builders/jest": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@angular-builders/jest/-/jest-17.0.3.tgz", + "integrity": "sha512-LW4s8t+NLnWR7Aud+EZup8dOBfQF8rfOIncsarDtP/48rz/Ucnzvum7xEt/NYAlZ6y/Dpk7wO6SlqAsaOPf8mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-builders/common": "1.0.2", + "@angular-devkit/architect": ">=0.1700.0 < 0.1800.0", + "@angular-devkit/core": "^17.0.0", + "jest-preset-angular": "14.0.3", + "lodash": "^4.17.15" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular-devkit/build-angular": "^17.0.0", + "@angular/compiler-cli": "^17.0.0", + "@angular/core": "^17.0.0", + "@angular/platform-browser-dynamic": "^17.0.0", + "jest": ">=29" + } + }, + "node_modules/@angular-builders/jest/node_modules/jest-preset-angular": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.0.3.tgz", + "integrity": "sha512-usgBL7x0rXMnMSx8iEFeOozj50W6fp+YAmQcQBUdAXhN+PAXRy4UXL6I/rfcAOU09rnnq7RKsLsmhpp/fFEuag==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "esbuild-wasm": ">=0.15.13", + "jest-environment-jsdom": "^29.0.0", + "jest-util": "^29.0.0", + "pretty-format": "^29.0.0", + "ts-jest": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0" + }, + "optionalDependencies": { + "esbuild": ">=0.15.13" + }, + "peerDependencies": { + "@angular-devkit/build-angular": ">=15.0.0 <18.0.0", + "@angular/compiler-cli": ">=15.0.0 <18.0.0", + "@angular/core": ">=15.0.0 <18.0.0", + "@angular/platform-browser-dynamic": ">=15.0.0 <18.0.0", + "jest": "^29.0.0", + "typescript": ">=4.8" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1703.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.10.tgz", + "integrity": "sha512-wmjx5GspSPprdUGryK5+9vNawbEO7p8h9dxgX3uoeFwPAECcHC+/KK3qPhX2NiGcM6MDsyt25SrbSktJp6PRsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.10", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.7.tgz", + "integrity": "sha512-AsV80kiFMIPIhm3uzJgOHDj4u6JteUkZedPTKAFFFJC7CTat1luW5qx306vfF7wj62aMvUl5g9HFWaeLghTQGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1703.7", + "@angular-devkit/build-webpack": "0.1703.7", + "@angular-devkit/core": "17.3.7", + "@babel/core": "7.24.0", + "@babel/generator": "7.23.6", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.22.6", + "@babel/plugin-transform-async-generator-functions": "7.23.9", + "@babel/plugin-transform-async-to-generator": "7.23.3", + "@babel/plugin-transform-runtime": "7.24.0", + "@babel/preset-env": "7.24.0", + "@babel/runtime": "7.24.0", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "17.3.7", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.18", + "babel-loader": "9.1.3", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.22", + "css-loader": "6.10.0", + "esbuild-wasm": "0.20.1", + "fast-glob": "3.3.2", + "http-proxy-middleware": "2.0.6", + "https-proxy-agent": "7.0.4", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.8", + "mini-css-extract-plugin": "2.8.1", + "mrmime": "2.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.1", + "piscina": "4.4.0", + "postcss": "8.4.35", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.71.1", + "sass-loader": "14.1.1", + "semver": "7.6.0", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.29.1", + "tree-kill": "1.2.2", + "tslib": "2.6.2", + "undici": "6.11.1", + "vite": "5.1.7", + "watchpack": "2.4.0", + "webpack": "5.90.3", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.15.1", + "webpack-merge": "5.10.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.20.1" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "@angular/localize": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/service-worker": "^17.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^17.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.2 <5.5" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", + "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.7.tgz", + "integrity": "sha512-gpt2Ia5I1gmdp3hdbtB7tkZTba5qWmKeVhlCYswa/LvbceKmkjedoeNRAoyr1UKM9GeGqt6Xl1B2eHzCH+ykrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1703.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", + "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.10.tgz", + "integrity": "sha512-czdl54yxU5DOAGy/uUPNjJruoBDTgwi/V+eOgLNybYhgrc+TsY0f7uJ11yEk/pz5sCov7xIiS7RdRv96waS7vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.7.tgz", + "integrity": "sha512-d7NKSwstdxYLYmPsbcYO3GOFNfXxXwOyHxSqDa1JNKoSzMdbLj4tvlCpfXw0ThNM7gioMx8aLBaaH1ac+yk06Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.7", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "17.5.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.5.2.tgz", + "integrity": "sha512-K4hVnMyI98faMJmsA4EOBkD0tapDjWV5gy0j/wJ2uSL46d3JgZPZNJSO1zStf/b3kT4gLOlQ/ulWFiUf1DxgIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "17.5.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.5.2.tgz", + "integrity": "sha512-kzPALKyT5XIEbgoNmY/hEZWdMWKTX56Pap9fVLJSC/b+Nd+MXc7TNly2s0XoC0Ru1/kMiVzbmSGPheu/rw+9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.5.2", + "@angular-eslint/utils": "17.5.2", + "@typescript-eslint/utils": "7.11.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "17.5.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.5.2.tgz", + "integrity": "sha512-bTMPFqtoetBJsYR/xqREEOCy/CdsKGf2gZkRdH73gG6pOpskWt8J/PbRcMZsC349paV4HFixByVm89inqA0TNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.5.2", + "@typescript-eslint/utils": "7.11.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.9.tgz", + "integrity": "sha512-9fSFF9Y+pKZGgGEK3IlVy9msS7LRFpD1h2rJ80N6n1k51jiKcTgOcFPPYwLNJZ2fkp+qrOAMo3ez4WYQgVPoow==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.9" + } + }, + "node_modules/@angular/cdk": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.9.tgz", + "integrity": "sha512-N/7Is+FkIIql5UEL/I+PV6THw+yXNCCGGpwimf/yaNgT9y1fHAmBWhDY0oQqFjCuD+kXl9gQL0ONfsl5Nlnk+w==", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.7.tgz", + "integrity": "sha512-JgCav3sdRCoJHwLXxmF/EMzArYjwbqB+AGUW/xIR98oZET8QxCB985bOFUAm02SkAEUVcMJvjxec+WCaa60m/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1703.7", + "@angular-devkit/core": "17.3.7", + "@angular-devkit/schematics": "17.3.7", + "@schematics/angular": "17.3.7", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.2", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "npm-package-arg": "11.0.1", + "npm-pick-manifest": "9.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "17.0.6", + "resolve": "1.22.8", + "semver": "7.6.0", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", + "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/common": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.9.tgz", + "integrity": "sha512-tH1VfbAvNVaz6ZYa+q0DiKtbmUql1jK/3q/af74B8nVjKLHcXVWwxvBayqvrmlUt7FGANGkETIcCWrB44k47Ug==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.9", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.9.tgz", + "integrity": "sha512-2d4bPbNm7O2GanqCj5GFgPDnmjbAcsQM502Jnvcv7Aje82yecT69JoqAVRqGOfbbxwlJiPhi31D8DPdLaOz47Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.9" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.9.tgz", + "integrity": "sha512-J6aqoz5wqPWaurbZFUZ7iMUlzAJYXzntziJJbalm6ceXfUWEe2Vm67nGUROWCIFvO3kWXvkgYX4ubnqtod2AxA==", + "license": "MIT", + "dependencies": { + "@babel/core": "7.23.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.3.9", + "typescript": ">=5.2 <5.5" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.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.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "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==", + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.9.tgz", + "integrity": "sha512-x+h5BQ6islvYWGVLTz1CEgNq1/5IYngQ+Inq/tWayM6jN7RPOCydCCbCw+uOZS7MgFebkP0gYTVm14y1MRFKSQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, + "node_modules/@angular/forms": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.9.tgz", + "integrity": "sha512-5b8OjK0kLghrdxkVWglgerHVp9D5WvXInXwo1KIyc2v/fGdTlyu/RFi0GLGvzq2y+7Z8TvtXWC82SB47vfx3TQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.9", + "@angular/core": "17.3.9", + "@angular/platform-browser": "17.3.9", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/localize": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.3.9.tgz", + "integrity": "sha512-ECWWw6GoJh2laopHIf+QT4bDDpSWwQJk95SGPI5mQIEXZXw6w9ms05Sfb8KJTNRXs9kcotloIGK9YanFZxzK1g==", + "license": "MIT", + "dependencies": { + "@babel/core": "7.23.9", + "@types/babel__core": "7.20.5", + "fast-glob": "3.3.2", + "yargs": "^17.2.1" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.3.9", + "@angular/compiler-cli": "17.3.9" + } + }, + "node_modules/@angular/localize/node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.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.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/localize/node_modules/convert-source-map": { + "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==", + "license": "MIT" + }, + "node_modules/@angular/localize/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/material": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.9.tgz", + "integrity": "sha512-iNLXGfTsXYQ7lX9UkU4ifb6+lVKjDFQJkWE8HmNWC3C2KXC9k15UefPKy4/sZUVzLE/yOBHPfNDwdhaJGlcu+g==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/auto-init": "15.0.0-canary.7f224ddd4.0", + "@material/banner": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/card": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/chips": "15.0.0-canary.7f224ddd4.0", + "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", + "@material/data-table": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dialog": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/drawer": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/fab": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/form-field": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/image-list": "15.0.0-canary.7f224ddd4.0", + "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/radio": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/slider": "15.0.0-canary.7f224ddd4.0", + "@material/snackbar": "15.0.0-canary.7f224ddd4.0", + "@material/switch": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/textfield": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tooltip": "15.0.0-canary.7f224ddd4.0", + "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^17.0.0 || ^18.0.0", + "@angular/cdk": "17.3.9", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", + "@angular/platform-browser": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.9.tgz", + "integrity": "sha512-vMwHO76rnkz7aV3KHKy23KUFAo/+b0+yHPa6AND5Lee8z5C1J/tA2PdetFAsghlQQsX61JeK4MFJV/f3dFm2dw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.3.9", + "@angular/common": "17.3.9", + "@angular/core": "17.3.9" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.9.tgz", + "integrity": "sha512-Jmth4hFC4dZsWQRkxB++42sR1pfJUoQbErANrKQMgEPb8H4cLRdB1mAQ6f+OASPBM+FsxDxjXq2kepyLGtF2Vg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.9", + "@angular/compiler": "17.3.9", + "@angular/core": "17.3.9", + "@angular/platform-browser": "17.3.9" + } + }, + "node_modules/@angular/router": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.9.tgz", + "integrity": "sha512-0cRF5YBJoDbXGQsRs3wEG+DPvN4PlhEqTa0DkTr9QIDJRg5P1uiDlOclV+w3OxEMsLrmXGmhjauHaWQk07M4LA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.9", + "@angular/core": "17.3.9", + "@angular/platform-browser": "17.3.9", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/service-worker": { + "version": "17.3.9", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-17.3.9.tgz", + "integrity": "sha512-7KFThQMTwEj/yj/Sk2NvdsCpBf46wV121UvmDYIaUl8AQcnrD94W+BJHkPSw1WgPL4yLquySg6D6GGHQcL2dQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "bin": { + "ngsw-config": "ngsw-config.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.9", + "@angular/core": "17.3.9" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.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.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "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==", + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz", + "integrity": "sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz", + "integrity": "sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz", + "integrity": "sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-wrap-function": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "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, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz", + "integrity": "sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz", + "integrity": "sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz", + "integrity": "sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz", + "integrity": "sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "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-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", + "integrity": "sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "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-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, + "license": "MIT", + "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.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz", + "integrity": "sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz", + "integrity": "sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz", + "integrity": "sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz", + "integrity": "sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz", + "integrity": "sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/template": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz", + "integrity": "sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz", + "integrity": "sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz", + "integrity": "sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz", + "integrity": "sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", + "integrity": "sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz", + "integrity": "sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz", + "integrity": "sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz", + "integrity": "sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz", + "integrity": "sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz", + "integrity": "sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz", + "integrity": "sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz", + "integrity": "sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz", + "integrity": "sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz", + "integrity": "sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz", + "integrity": "sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz", + "integrity": "sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", + "integrity": "sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz", + "integrity": "sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", + "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz", + "integrity": "sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz", + "integrity": "sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz", + "integrity": "sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz", + "integrity": "sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz", + "integrity": "sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz", + "integrity": "sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz", + "integrity": "sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz", + "integrity": "sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz", + "integrity": "sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@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-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "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, + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.3.tgz", + "integrity": "sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/basic-setup": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz", + "integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==", + "deprecated": "In version 6.0, this package has been renamed to just 'codemirror'", + "dependencies": { + "@codemirror/autocomplete": "^0.20.0", + "@codemirror/commands": "^0.20.0", + "@codemirror/language": "^0.20.0", + "@codemirror/lint": "^0.20.0", + "@codemirror/search": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/autocomplete": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz", + "integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/commands": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz", + "integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/language": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz", + "integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0", + "@lezer/highlight": "^0.16.0", + "@lezer/lr": "^0.16.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/lint": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz", + "integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.2", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/search": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz", + "integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/state": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==" + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/view": { + "version": "0.20.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", + "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/common": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz", + "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==" + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/highlight": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz", + "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/lr": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz", + "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", + "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", + "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", + "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz", + "integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/view": { + "version": "6.34.3", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.3.tgz", + "integrity": "sha512-Ph5d+u8DxIeSgssXEakaakImkzBV4+slwIbcxl9oc9evexJhImeu/G8TK7+zp+IFK9KuJ0BdSn6kTBJeH2CHvA==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, + "node_modules/@fortawesome/angular-fontawesome": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.14.1.tgz", + "integrity": "sha512-Yb5HLiEOAxjSLEcaOM51CKIrzdfvoDafXVJERm9vufxfZkVZPZJgrZRgqwLVpejgq4/Ez6TqHZ6SqmJwdtRF6g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/core": "^17.0.0", + "@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "hasInstallScript": true, + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", + "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "ISC", + "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/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, + "license": "MIT", + "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, + "license": "MIT", + "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/console/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "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/@jest/reporters/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/transform/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "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, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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/@jest/types/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-1GSJaPKef+7HRuV+HusVZHps64cmZuOItDbt40tjJVaikcaZvwmHlcTxRIqzcRoCdt5ZKHh3NoO7GB9Khg4Jnw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-t7ZGpRJ3ec0QDUO0nJu/SMgLW7qcuG2KqIsEYD1Ej8qhI2xpdR2ydSDQOkVEitXmKoGol1oq4nYSBjTlB65GqA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-g9wBUZzYBizyBcBQXTIafnRUUPi7efU9gPJfzeGgkynXiccP/vh5XMmH+PBxl5v+4MlP/d4cZ2NUYoAN7UTqSA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-I9KQOKXpLfJkP8MqZyr8wZIzdPHrwPjFvGd9zSK91/vPyE4hzHRJc/0njsh9g8Lm9PRYLbifXX+719uTbHxx+A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BHB7iyHgRVH+JF16+iscR+Qaic+p7LU1FOLgP8KucRlpF9tTwIxQA6mJwGRi5gUtcG+vyCmzVS+hIQ6DqT/7BA==", + "license": "MIT", + "dependencies": { + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kt7y9/IWOtJTr3Z/AoWJT3ZLN7CLlzXhx2udCLP9ootZU2bfGK0lzNwmo80bv/pJfrY9ihQKCtuGTtNxUy+vIw==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-rURcrL5O1u6hzWR+dNgiQ/n89vk6tdmdP3mZgnxJx61q4I/k1yijKqNJSLrkXH7Rto3bM5NRKMOlgvMvVd7UMQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AYAivV3GSk/T/nRIpH27sOHFPaSMrE3L0WYbnb5Wa93FgY8a0fbsFYtSH2QmtwnzXveg+B1zGTt7/xIIcynKdQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DJrqCKb+LuGtjNvKl8XigvyK02y36GRkfhMUYTcJEi3PrOE00bwXtyj7ilhzEVshQiXg6AHGWXtf5UqwNrx3Ow==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-/2WZsuBIq9z9RWYF5Jo6b7P6u0fwit+29/mN7rmAZ6akqUR54nXyNfoSNiyydMkzPlZZsep5KrSHododDhBZbA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-o9EXmGKVpiQ6mHhyV3oDDzc78Ow3E7v8dlaOhgaDSXgmqaE8v5sIlLNa/LKSyUga83/fpGk3QViSGXotpQx0jA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-u0XpTlv1JqWC/bQ3DavJ1JguofTelLT2wloj59l3/1b60jv42JQ6Am7jU3I8/SIUB1MKaW7dYocXjDWtWJakLA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-mQ1HT186GPQSkRg5S18i70typ5ZytfjL09R0gJ2Qg5/G+MLCGi7TAjZZSH65tuD/QGOjel4rDdWOTmYbPYV6HA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-qyO0W0KBftfH8dlLR0gVAgv7ZHNvU8ae11Ao6zJif/YxcvK4+gph1z8AO4H410YmC2kZiwpSKyxM1iQCCzbb4g==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-tV6s4/pUBECedaI36Yj18KmRCk1vfue/JP/5yYRlFNnLMRVISePbZaKkn/BHXVf+26I3W879+XqIGlDVdmOoMA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-4h76QrzfZTcPdd+awDPZ4Q0YdSqsXQnS540TPtyXUJ/5G99V6VwGpjMPIxAsW0y+pmI9UkLL/srrMaJec+7r4Q==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SAjtxYh6YlKZriU83diDEQ7jNSP2MnxKsER0TvFeyG1vX/DWsUyYDOIJTOEa9K1N+fgJEBkNK8hY55QhQaspew==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-0KMo5ijjYaEHPiZ2pCVIcbaTS2LycvH9zEhEMKwPPGssBCX7iz5ffYQFk7e5yrQand1r3jnQQgYfHAwtykArnQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Jmg1nltq4J6S6A10EGMZnvufrvU3YTi+8R8ZD9lkSbun0Fm2TVdICQt/Auyi6An9zP66oQN6c31eqO6KfIPsDg==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-fEPWgDQEPJ6WF7hNnIStxucHR9LE4DoDSMqCsGWS2Yu+NLZYLuCEecgR0UqQsl1EQdNRaFh8VH93KuxGd2hiPg==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DcK7IL4ICY/DW+48YQZZs9g0U1kRaW0Wb0BxhvppDMYziHo/CTpFdle4gjyuTyRxPOdHQz5a97ru48Z9O4muTw==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-voMjG2p80XbjL1B2lmF65zO5gEgJOVKClLdqh4wbYzYfwY/SR9c8eLvlYG7DLdFaFBl/7gGxD8TvvZ329HUFPw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-veDABLxMn2RmvfnUO2RUmC1OFfWr4cU+MrxKPoDD2hl3l3eDYv5fxws6r5T1JoSyXoaN+oEZpheS0+M9Ure8Pg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-f60hVJhIU6I3/17Tqqzch1emUKEcfVVgHVqADbU14JD+oEIz429ZX9ksZ3VChoU3+eejFl+jVdZMLE/LrAuwpg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-pRDEwPQielDiC9Sc5XhCXrGxP8wWOnAO8sQlMebfBYHYqy5hhiIzibezS8CSaW4MFQFyXmCmpmqWlbqGYRmiyg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Is0NV91sJlXF5pOebYAtWLF4wU2MJDbYqztML/zQNENkQxDOvEXu3nWNb3YScMIYJJXvARO0Liur5K4yPagS1Q==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-D11QU1dXqLbh5X1zKlEhS3QWh0b5BPNXlafc5MXfkdJHhOiieb7LC9hMJhbrHtj24FadJ7evaFW/T2ugJbJNnQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-7RZHvw0gbwppaAJ/Oh5SWmfAKJ62aw1IMB3+3MRwsb5PLoV666wInYa+zJfE4i7qBeOn904xqT2Nko5hY0ssrg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Yg2usuKB2DKlKIBISbie9BFsOVuffF71xjbxPbybvqemxqUBd+bD5/t6H1fLE+F8/NCu5JMigho4ewUU+0RCiw==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UPbDjE5CqT+SqTs0mNFG6uFEw7wBlgYmh+noSkQ6ty/EURm8lF125dmi4dv4kW0+octonMXqkGtAoZwLIHKf/w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-wR1X0Sr0KmQLu6+YOFKAI84G3L6psqd7Kys5kfb8WKBM36zxO5HQXC5nJm/Y0rdn22ixzsIz2GBo0MNU4V4k1A==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-JqOsWM1f4aGdotP0rh1vZlPZTg6lZgh39FIYHFMfOwfhR+LAikUJ+37ciqZuewgzXB6iiRO6a8aUH6HR5SJYPg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UVf14qAtmPiaaZjuJtmN36HETyoKWmsZM/qn1L5ciR2URb8O035dFWnz4ZWFMmAYBno/L7JiZaCkPurv2ZNrGA==", + "license": "MIT", + "dependencies": { + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LCnVRUSAhELTKI/9hSvyvIvQIpPpqF29BV+O9yM4WoNNmNWqTulvuiv7grHZl6Z+kJuxSg4BGbsPxxb9dXozPg==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-WioZtQEXRpglum0cMSzSqocnhsGRr+ZIhvKb3FlaNrTaK8H3Y4QA7rVjv3emRtrLOOjaT6/RiIaUMTo9AGzWQQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8z8l1W3+cymObunJoRhwFPKZ+FyECfJ4MJykNiaZq7XJFZkV6xNmqAVrrbQj93FtLsECn9g4PjjIomguVn/OEw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-QU/WSaSWlLKQRqOhJrPgm29wqvvzRusMqwAcrCh1JTrCl+xwJ43q5WLDfjYhubeKtrEEgGu9tekkAiYfMG7EBw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-sm7EbVKddaXpT/aXAYBdPoN0k8yeg9+dprgBUkrdqGzWJAeCkxb4fv2B3He88YiCtvkTz2KLY4CThPQBSEsMFQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-lEDJfRvkVyyeHWIBfoxYjJVl+WlEAE2kZ/+6OqB1FW0OV8ftTODZGhHRSzjVBA1/p4FPuhAtKtoK9jTpa4AZjA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-E1xGACImyCLurhnizyOTCgOiVezce4HlBFAI6YhJo/AyVwjN2Dtas4ZLQMvvWWqpyhITNkeYdOchwCC1mrz3AQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-p1Asb2NzrcECvAQU3b2SYrpyJGyJLQWR+nXTYzDKE8WOpLIRCXap2audNqD7fvN/A20UJ1J8U01ptrvCkwJ4eA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-h9Td3MPqbs33spcPS7ecByRHraYgU4tNCZpZzZXw31RypjKvISDv/PS5wcA4RmWqNGih78T7xg4QIGsZg4Pk4w==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LFeYNjQpdXecwECd8UaqHYbhscDCwhGln5Yh+3ctvcEgvmDPNjhKn/DL3sWprWvG8NAhP6sHMrsGhQFVdCWtTg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AExmFvgE5nNF0UA4l2cSzPghtxSUQeeoyRjFLHLy+oAaE4eKZFrSy0zEpqPeWPQpEMDZk+6Y+6T3cOFYBeSvsw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-hs45hJoE9yVnoVOcsN1jklyOa51U4lzWsEnQEuJTPOk2+0HqCQ0yv/q0InpSnm2i69fNSyZC60+8HADZGF8ugQ==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-r9TDoicmcT7FhUXC4eYMFnt9TZsz0G8T3wXvkKncLppYvZ517gPyD/1+yhuGfGOxAzxTrM66S/oEc1fFE2q4hw==", + "license": "MIT", + "dependencies": { + "@material/elevation": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8qNk3pmPLTnam3XYC1sZuplQXW9xLn4Z4MI3D+U17Q7pfNZfoOugGr+d2cLA9yWAEjVJYB0mj8Yu86+udo4N9w==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SARR5/ClYT4CLe9qAXakbr0i0cMY0V3V4pe3ElIJPfL2Z2c4wGR1mTR8m2LxU1MfGKK8aRoUdtfKaxWejp+eNA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BJo/wFKHPYLGsRaIpd7vsQwKr02LtO2e89Psv0on/p0OephlNIgeB9dD9W+bQmaeZsZ6liKSKRl6wJWDiK71PA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kBaZeCGD50iq1DeRRH5OM5Jl7Gdk+/NOfKArkY4ksBZvJiStJ7ACAhpvb8MEGm4s3jvDInQFLsDq3hL+SA79sQ==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@ng-bootstrap/ng-bootstrap": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-16.0.0.tgz", + "integrity": "sha512-+FJ3e6cX9DW2t7021Ji3oz433rk3+4jLfqzU+Jyx6/vJz1dIOaML3EAY6lYuW4TLiXgMPOMvs6KzPFALGh4Lag==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.0", + "@angular/core": "^17.0.0", + "@angular/forms": "^17.0.0", + "@angular/localize": "^17.0.0", + "@popperjs/core": "^2.11.8", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ng-select/ng-select": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-12.0.0.tgz", + "integrity": "sha512-cnpPMwu46JR+LaHfQgCfked74tlNK9WDWsSiLtoRK0HkK0MJ9FFZvumPwEUKfkTWHcmD3HV3fgiQnRXrjnntNA==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 16", + "npm": ">= 8" + }, + "peerDependencies": { + "@angular/common": "^17.0.0-rc.0", + "@angular/core": "^17.0.0-rc.0", + "@angular/forms": "^17.0.0-rc.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.7.tgz", + "integrity": "sha512-kQNS68jsPQlaWAnKcVeFKNHp6K90uQANvq+9oXb/i+JnYWzuBsHzn2r8bVdMmvjd1HdBRiGtg767XRk3u+jgRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "typescript": ">=5.2 <5.5", + "webpack": "^5.54.0" + } + }, + "node_modules/@ngx-translate/core": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-15.0.0.tgz", + "integrity": "sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA==", + "license": "SEE LICENSE IN LICENSE", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-8.0.0.tgz", + "integrity": "sha512-SFMsdUcmHF5OdZkL1CHEoSAwbP5EbAOPTLLboOCRRoOg21P4GJx+51jxGdJeGve6LSKLf4Pay7BkTwmE6vxYlg==", + "license": "SEE LICENSE IN LICENSE", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ngx-translate/core": ">=15.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/arborist": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.4.tgz", + "integrity": "sha512-nWtIc6QwwoUORCRNzKx4ypHqCk3drI+5aeYdMTQQiRCcn4lOOgfQh7WyZobGYTxXPSq1VwV53lkpN/BRlRk08g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.1", + "@npmcli/installed-package-contents": "^2.1.0", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^7.1.1", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.1.0", + "@npmcli/query": "^3.1.0", + "@npmcli/redact": "^2.0.0", + "@npmcli/run-script": "^8.1.0", + "bin-links": "^4.0.4", + "cacache": "^18.0.3", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^7.0.2", + "json-parse-even-better-errors": "^3.0.2", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^7.2.1", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^11.0.2", + "npm-pick-manifest": "^9.0.1", + "npm-registry-fetch": "^17.0.1", + "pacote": "^18.0.6", + "parse-conflict-json": "^3.0.0", + "proc-log": "^4.2.0", + "proggy": "^2.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.6", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/arborist/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/arborist/node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz", + "integrity": "sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-7.1.1.tgz", + "integrity": "sha512-Nkxf96V0lAx3HCpVda7Vw4P23RILgdi/5K1fmj2tZkWIYLpXAN8k2UVVOsW16TsS5F8Ws2I7Cm+PU1/rsVF47g==", + "dev": true, + "license": "ISC", + "dependencies": { + "cacache": "^18.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^18.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", + "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/query": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-3.1.0.tgz", + "integrity": "sha512-C/iR0tk7KSKGldibYIB9x8GtO/0Bd0I2mhOaDb8ucQL/bQVTmGoeREaFj64Z5+iCBRf3dQfed0CjJL7I8iTkiQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", + "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", + "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.40.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/request/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/rest": { + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^3.5.1", + "@octokit/plugin-paginate-rest": "^2.16.8", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.7.tgz", + "integrity": "sha512-HaJroKaberriP4wFefTTSVFrtU9GMvnG3I6ELbOteOyKMH7o2V91FXGJDJ5KnIiLRlBmC30G3r+9Ybc/rtAYkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.7", + "@angular-devkit/schematics": "17.3.7", + "jsonc-parser": "3.2.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", + "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tinymce/tinymce-angular": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@tinymce/tinymce-angular/-/tinymce-angular-8.0.1.tgz", + "integrity": "sha512-m0/ne5nL00YeZ7wCyhBVwKUbvS8fQZ+S+T5pinqTIqTUKpcfIC2+BIKqzUpS+niiRCowFoZl5eVlI5zdFN9/0A==", + "dependencies": { + "tinymce": "^7.0.0 || ^6.0.0 || ^5.5.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@angular/forms": ">=16.0.0", + "rxjs": "^7.4.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.15", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", + "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" + }, + "node_modules/@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.11.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", + "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/quill": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-2.0.14.tgz", + "integrity": "sha512-zvoXCRnc2Dl8g+7/9VSAmRWPN6oH+MVhTPizmCR+GJCITplZ5VRVzMs4+a/nOE3yzNwEZqylJJrMB07bwbM1/g==", + "dev": true, + "dependencies": { + "parchment": "^1.1.2", + "quill-delta": "^5.1.0" + } + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vinyl": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", + "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", + "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/type-utils": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", + "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", + "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/@yeoman/adapter": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@yeoman/adapter/-/adapter-1.4.0.tgz", + "integrity": "sha512-JroPWaZ8fALkfRt1FVM8/jz0kGOviVkKaCR4y0EM9Si2B9UD4UySGLCrjyUWeWBGqgr2iGAQ0ehoHjRAlyzsFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/inquirer": "^9.0.3", + "chalk": "^5.2.0", + "inquirer": "^9.2.2", + "log-symbols": "^5.1.0", + "ora": "^6.3.1", + "p-queue": "^7.3.4", + "text-table": "^0.2.0" + }, + "engines": { + "node": "^16.13.0 || >=18.12.0" + }, + "peerDependencies": { + "@yeoman/types": "^1.1.0" + } + }, + "node_modules/@yeoman/adapter/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@yeoman/adapter/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@yeoman/adapter/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@yeoman/adapter/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@yeoman/adapter/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@yeoman/adapter/node_modules/ora": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz", + "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@yeoman/adapter/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@yeoman/adapter/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, + "license": "ISC" + }, + "node_modules/@yeoman/adapter/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@yeoman/conflicter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@yeoman/conflicter/-/conflicter-2.0.0.tgz", + "integrity": "sha512-DhxzWfHXg+W3AGyWM35L2o4GkQbUcT30f2+l6/2sZGwQcUPyTIR9RDyxrV9pf6YlwUJwvKjL2jLdB2QlJ1mKbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^16.18.28", + "@yeoman/transform": "^1.2.0", + "binary-extensions": "^2.2.0", + "cli-table": "^0.3.11", + "dateformat": "^5.0.3", + "diff": "^5.1.0", + "isbinaryfile": "^5.0.0", + "mem-fs-editor": "^11.0.0", + "minimatch": "^9.0.0", + "p-transform": "^4.1.3", + "pretty-bytes": "^6.1.0", + "textextensions": "^5.16.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@yeoman/types": "^1.0.0", + "mem-fs": "^4.0.0" + } + }, + "node_modules/@yeoman/conflicter/node_modules/@types/node": { + "version": "16.18.113", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.113.tgz", + "integrity": "sha512-4jHxcEzSXpF1cBNxogs5FVbVSFSKo50sFCn7Xg7vmjJTbWFWgeuHW3QnoINlfmfG++MFR/q97RZE5RQXKeT+jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@yeoman/namespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@yeoman/namespace/-/namespace-1.0.0.tgz", + "integrity": "sha512-+HcGOOoLSP3+Hb3xA3TpYDiSsmok/boJtbd4bhNfKGDp9/bXkSBpK0Bqmydl0ulo4rUGwiY95eVtP2sLpoDGlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.13.0 || >=18.12.0" + } + }, + "node_modules/@yeoman/transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@yeoman/transform/-/transform-1.2.0.tgz", + "integrity": "sha512-evb/+2XMEBoHr4BxBeFkjeVTgTS4Qe7VH8DmzZ9kgJK7C7ACPAhW/qBdsKKP1sb5MoeITSaJSVFnc8S1fjZmcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^16.18.28", + "minimatch": "^9.0.0", + "readable-stream": "^4.3.0" + }, + "engines": { + "node": "^16.13.0 || >=18.12.0" + } + }, + "node_modules/@yeoman/transform/node_modules/@types/node": { + "version": "16.18.113", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.113.tgz", + "integrity": "sha512-4jHxcEzSXpF1cBNxogs5FVbVSFSKo50sFCn7Xg7vmjJTbWFWgeuHW3QnoINlfmfG++MFR/q97RZE5RQXKeT+jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@yeoman/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@yeoman/types/-/types-1.2.0.tgz", + "integrity": "sha512-Ofaig2hSrauCZ5ZTn9paWtgeG1vJseVsrzToIo/ub3bnm4IDwNf/cQv1/qkvhYSns+xnq7CR5u8kr1fFcMcKfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^16.18.26" + }, + "acceptDependencies": { + "mem-fs": "^4.0.0-beta.1", + "mem-fs-editor": ">=10.0.2" + }, + "engines": { + "node": "^16.13.0 || >=18.12.0" + }, + "peerDependencies": { + "@types/inquirer": "^9.0.3", + "mem-fs": "^3.0.0", + "mem-fs-editor": "^10.0.2" + }, + "peerDependenciesMeta": { + "inquirer": { + "optional": true + }, + "mem-fs": { + "optional": true + }, + "mem-fs-editor": { + "optional": true + } + } + }, + "node_modules/@yeoman/types/node_modules/@types/node": { + "version": "16.18.113", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.113.tgz", + "integrity": "sha512-4jHxcEzSXpF1cBNxogs5FVbVSFSKo50sFCn7Xg7vmjJTbWFWgeuHW3QnoINlfmfG++MFR/q97RZE5RQXKeT+jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-differ": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-4.0.0.tgz", + "integrity": "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz", + "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomically": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", + "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==", + "dev": true, + "dependencies": { + "stubborn-fs": "^1.2.5", + "when-exit": "^2.1.1" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "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, + "license": "MIT", + "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-jest/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "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, + "license": "BSD-3-Clause", + "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-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, + "license": "MIT", + "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-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@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-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "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, + "license": "MIT", + "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", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bin-links": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz", + "integrity": "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/bin-links/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binaryextensions": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-4.19.0.tgz", + "integrity": "sha512-DRxnVbOi/1OgA5pA9EDiRT8gvVYeqfuN7TmPfLyt6cyho3KbHCi3EtDQf39TTmGDrR5dZ9CspdXhPkL/j/WGbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "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, + "license": "MIT", + "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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.2.tgz", + "integrity": "sha512-PC9c7aWJFVR4IFySrJxOqLwB9ENn3/TaXCXtAa0SzLwocLN3qMjN+IatbjvtCX92BjNXsY6YWg9Eb7F3Wy255g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "browser-sync-client": "^3.0.2", + "browser-sync-ui": "^3.0.2", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.0.1", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "micromatch": "^4.0.2", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "0.16.2", + "serve-index": "1.9.1", + "serve-static": "1.13.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" + }, + "bin": { + "browser-sync": "dist/bin.js" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/browser-sync-client": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/browser-sync-ui": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" + } + }, + "node_modules/browser-sync-ui/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync-ui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/browser-sync-ui/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync-ui/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync-webpack-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/browser-sync-webpack-plugin/-/browser-sync-webpack-plugin-2.3.0.tgz", + "integrity": "sha512-MDvuRrTCtoL11dTdwMymo9CNJvYxJoW67gOO61cThfzHNX40S5WcBU+0bVQ86ll7r7aNpNgyzxF7RtnXMTDbyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4" + }, + "peerDependencies": { + "browser-sync": "^2", + "webpack": "^1 || ^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/browser-sync/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/browser-sync/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "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.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "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, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "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": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "engines": { + "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, + "license": "MIT" + }, + "node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "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, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "dev": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cliui/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==", + "license": "MIT", + "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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/cmd-shim": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.3.tgz", + "integrity": "sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.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, + "license": "MIT" + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "dev": true, + "license": "ISC" + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/conf": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-12.0.0.tgz", + "integrity": "sha512-fIWyWUXrJ45cHCIQX+Ck1hrZDIf/9DR0P0Zewn3uNht28hbt5OfGUq8rRWsxi96pZWPyBEd0eY9ama01JTaknA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "atomically": "^2.0.2", + "debounce-fn": "^5.1.2", + "dot-prop": "^8.0.2", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.5.4", + "uint8array-extras": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js-compat": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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": { + "@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/create-jest/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, + "node_modules/critters": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", + "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/critters/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dateformat": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-5.0.3.tgz", + "integrity": "sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debounce-fn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-5.1.2.tgz", + "integrity": "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "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/default-gateway/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/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, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/default-gateway/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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/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, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/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, + "license": "ISC" + }, + "node_modules/default-gateway/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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/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, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-indent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz", + "integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "dev": true, + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dockerfile-ast": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.6.1.tgz", + "integrity": "sha512-m3rH2qHHU2pSTCppXgJT+1KLxhvkdROOxVPof5Yz4IPGSw6K+x0B0/RFdYgXN5zsIUTlbOSRyfDCv3/uVhnNmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-8.0.2.tgz", + "integrity": "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-properties": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dot-properties/-/dot-properties-1.0.1.tgz", + "integrity": "sha512-cjIHHKlf2dPINJ5Io3lPocWvWmthXn3ztqyHVzUfufRiCiPECb0oiEqEGbEGaunFZtcMvwgUcxP9CTpLG4KCsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dev": true, + "dependencies": { + "lodash": "^4.17.10" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dev": true, + "dependencies": { + "chalk": "4.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eazy-logger/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eazy-logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eazy-logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eazy-logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.38", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.38.tgz", + "integrity": "sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==", + "license": "ISC" + }, + "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": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", + "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-import/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz", + "integrity": "sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "6 - 7", + "eslint": "8" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-rsfpFQ01AWQbqtjgPRr2usVRxhWDuG0YDYcG8DJOteD3EFnpeuYuOwk0PQiN7PRBTqS6ElNdtPZPggj8If9WnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^8.56.10", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.1.0.tgz", + "integrity": "sha512-lSgHc4Elo2m6bUDhc3Hl/VxvUDJdQWI40RZ4KMY9bKRc+hgMOT7II/JjbNDhI8VnMtrCb7U/fhpJIkLORZozWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^7.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^5.2.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "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==", + "dev": true, + "license": "MIT", + "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/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.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", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", + "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", + "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/first-chunk-stream": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-5.0.0.tgz", + "integrity": "sha512-WdHo4ejd2cG2Dl+sLkW79SctU7mUQDfr4s1i26ffOZRs5mgv+BRttIM9gwcq0rDbemo0KlpVPaa3LBVLqPXzcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/flat-cache/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fly-import": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fly-import/-/fly-import-0.4.1.tgz", + "integrity": "sha512-9gqEx0nnQ6SF0pKKOEexVNYCTBiLb3g+a5JMLkiaBIqwM/pEZH0Le83owUA/tkRLxWNwIso+sB3E+epuOCPWlw==", + "dev": true, + "dependencies": { + "@npmcli/arborist": "^7.2.0", + "env-paths": "^3.0.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/folder-hash": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-4.0.4.tgz", + "integrity": "sha512-zEyYH+UsHEfJJcCRSf9ai5I4CTZwZ8ObONRuEI5hcEmJY5pS0FUWKruX9mMnYJrgC7MlPFDYnGsK1R+WFYjLlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.3", + "minimatch": "~5.1.2" + }, + "bin": { + "folder-hash": "bin/folder-hash" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/folder-hash/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-jhipster": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/generator-jhipster/-/generator-jhipster-8.5.0.tgz", + "integrity": "sha512-sYxvxR3UCFVyQrEZO0ekr/tDu8p2U514p5g0WybTjUMYipe3ZwrlzsOfsqZ6zpX0dVtGFQ/A9+0dU1AMYJKJ5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@faker-js/faker": "8.4.1", + "@iarna/toml": "3.0.0", + "@types/ejs": "3.1.5", + "@types/lodash": "4.17.4", + "@typescript-eslint/eslint-plugin": "7.11.0", + "@typescript-eslint/parser": "7.11.0", + "@yeoman/adapter": "1.4.0", + "@yeoman/conflicter": "2.0.0", + "@yeoman/namespace": "1.0.0", + "@yeoman/transform": "1.2.0", + "@yeoman/types": "1.2.0", + "axios": "1.7.2", + "chalk": "5.3.0", + "chevrotain": "11.0.3", + "commander": "12.1.0", + "conf": "12.0.0", + "debug": "4.3.4", + "didyoumean": "1.2.2", + "dockerfile-ast": "0.6.1", + "dot-properties": "1.0.1", + "ejs": "3.1.10", + "eslint": "8.57.0", + "eslint-plugin-import": "2.29.1", + "eslint-plugin-unused-imports": "3.2.0", + "execa": "9.1.0", + "fast-xml-parser": "4.4.0", + "glob": "10.4.1", + "isbinaryfile": "5.0.2", + "java-lint": "0.3.0", + "js-yaml": "4.1.0", + "latest-version": "9.0.0", + "lodash-es": "4.17.21", + "mem-fs": "4.1.0", + "mem-fs-editor": "11.0.1", + "minimatch": "9.0.4", + "normalize-path": "3.0.0", + "os-locale": "6.0.2", + "p-transform": "4.1.5", + "parse-gitignore": "2.0.0", + "piscina": "4.5.1", + "pluralize": "8.0.0", + "prettier": "3.2.5", + "prettier-plugin-java": "2.6.0", + "prettier-plugin-packagejson": "2.5.0", + "prettier-plugin-properties": "0.3.0", + "randexp": "0.5.3", + "semver": "7.6.2", + "simple-git": "3.24.0", + "sort-keys": "5.0.0", + "type-fest": "4.18.3", + "typescript": "5.4.5", + "winston": "3.13.0", + "yaml": "2.4.2", + "yeoman-environment": "4.4.0", + "yeoman-generator": "7.2.0" + }, + "bin": { + "jhipster": "dist/cli/jhipster.cjs" + }, + "engines": { + "node": "^18.13.0 || >= 20.6.1", + "npm": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/generator-jhipster" + }, + "peerDependencies": { + "yeoman-test": "8.3.0" + }, + "peerDependenciesMeta": { + "yeoman-test": { + "optional": true + } + } + }, + "node_modules/generator-jhipster/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/generator-jhipster/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/generator-jhipster/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/generator-jhipster/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/generator-jhipster/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/generator-jhipster/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/generator-jhipster/node_modules/piscina": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.5.1.tgz", + "integrity": "sha512-DVhySLPfqAW+uRH9dF0bjA2xEWr5ANLAzkYXx5adSLMFnwssSIVJYhg0FlvgYsnT/khILQJ3WkjqbAlBvt+maw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/generator-jhipster/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/git-hooks-list": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.1.0.tgz", + "integrity": "sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/fisker/git-hooks-list?sponsor=1" + } + }, + "node_modules/github-username": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/github-username/-/github-username-7.0.0.tgz", + "integrity": "sha512-mzCjmmR1LcNf0/qvkJRO63di2lUUuEoRuCqzflq8wrpAajOo7zLSXOTTuj2qr1DhFY2pruw5JLw/CokZU/3ilg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/rest": "^18.12.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/grouped-queue": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grouped-queue/-/grouped-queue-2.0.0.tgz", + "integrity": "sha512-/PiFUa7WIsl48dUeCvhIHnwNmAAzlI/eHoJl0vu3nsFA366JleY7Ff8EVTplZu5kO0MIdZjKTTnzItL61ahbnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "dev": true, + "license": "MIT" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-7.0.0.tgz", + "integrity": "sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "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, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "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/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/index-to-position": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", + "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", + "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/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, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invert-kv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", + "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sindresorhus/invert-kv?sponsor=1" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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, + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BSD-3-Clause", + "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/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "license": "BSD-3-Clause", + "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/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/java-lint": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/java-lint/-/java-lint-0.3.0.tgz", + "integrity": "sha512-//tpg1P1J9ubTLJWxMrz7//GPfnAueB/UYb8zIsL5uibuKN2H+puWjfqa0KCUO+bKmK65Tcg1bZvFmn979atcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "java-parser": "~2.3.0" + }, + "engines": { + "node": "^18.13.0 || >= 20.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/marcelo-boveto-shima" + } + }, + "node_modules/java-parser": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-2.3.2.tgz", + "integrity": "sha512-/O42UbEHy3VVJw8W0ruHkQjW75oWvQx4QisoUDRIGir6q3/IZ4JslDMPMYEqp7LU56PYJkH5uXdQiBaCXt/Opw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chevrotain": "11.0.3", + "chevrotain-allstar": "0.3.1", + "lodash": "4.17.21" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@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": "^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-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, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "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/jest-changed-files/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/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, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/jest-changed-files/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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/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, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/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, + "license": "ISC" + }, + "node_modules/jest-changed-files/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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-date-mock": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/jest-date-mock/-/jest-date-mock-1.0.10.tgz", + "integrity": "sha512-g0CM7mJHppz8SCayrtJ0Wm2ge8T0SiKCR9bmVLeflipqWjZ8hieNk6vBF0t3dJFc5jlsjvzTbRud8kPjoD0VgA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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-diff/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "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-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, + "license": "MIT", + "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, + "license": "MIT", + "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-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "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, + "license": "MIT", + "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-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, + "license": "MIT", + "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-matcher-utils/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-preset-angular": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.1.0.tgz", + "integrity": "sha512-UJwPtpsAMl30UtBjHW0Ai0hhoKsNURC1dXH5tSYjumUsWR7iDke+oBEykz7uXv4rN+PWgeNIqkxo4KHQjOITlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "esbuild-wasm": ">=0.15.13", + "jest-environment-jsdom": "^29.0.0", + "jest-util": "^29.0.0", + "pretty-format": "^29.0.0", + "ts-jest": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0" + }, + "optionalDependencies": { + "esbuild": ">=0.15.13" + }, + "peerDependencies": { + "@angular-devkit/build-angular": ">=15.0.0 <19.0.0", + "@angular/compiler-cli": ">=15.0.0 <19.0.0", + "@angular/core": ">=15.0.0 <19.0.0", + "@angular/platform-browser-dynamic": ">=15.0.0 <19.0.0", + "jest": "^29.0.0", + "typescript": ">=4.8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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-resolve/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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-runtime/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-sonar": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/jest-sonar/-/jest-sonar-0.2.16.tgz", + "integrity": "sha512-ES6Z9BbIVDELtbz+/b6pv41B2qOfp38cQpoCLqei21FtlkG/GzhyQ0M3egEIM+erpJOkpRKM8Tc8/YQtHdiTXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "4.3.0", + "strip-ansi": "6.0.1" + } + }, + "node_modules/jest-sonar/node_modules/entities": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.3.0.tgz", + "integrity": "sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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-util/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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-watcher/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz", + "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-nice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", + "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/just-diff": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", + "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/just-diff-apply": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ky": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.7.2.tgz", + "integrity": "sha512-OzIvbHKKDpi60TnF9t7UUVAF1B4mcqc02z5PIvrm08Wyb+yOcz63GRvEuVxNT18a9E1SrNouhB4W2NNLeD7Ykg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/latest-version": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", + "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lcid": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", + "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "invert-kv": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true + }, + "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, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "15.2.5", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.5.tgz", + "integrity": "sha512-j+DfX7W9YUvdzEZl3Rk47FhDF6xwDBV5wwsCPw6BwWZVPYJemusQmvb9bRsW23Sqsaa+vRloAWogbK4BUuU2zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.4", + "execa": "~8.0.1", + "lilconfig": "~3.1.1", + "listr2": "~8.2.1", + "micromatch": "~4.0.7", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.4.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "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, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "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, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "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, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mem-fs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-4.1.0.tgz", + "integrity": "sha512-lOB7haBbxO43eZ/++GA+jBMHQ9DNJeliMt35jNutzCfAgEg5gblFCItnzsss8Z4t81bB5jsz77bptqelHQn0Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.8.3", + "@types/vinyl": "^2.0.8", + "vinyl": "^3.0.0", + "vinyl-file": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/mem-fs-editor": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-11.0.1.tgz", + "integrity": "sha512-ne7/ep9gIfl8IitTDBMlU2M0IRfvAzCK2zhoafu+hirqui9A9qp/KQOpG+J5/Td6qufbLee6RMxYeD5vxitK5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ejs": "^3.1.3", + "@types/node": "^18.18.5", + "binaryextensions": "^4.18.0", + "commondir": "^1.0.1", + "deep-extend": "^0.6.0", + "ejs": "^3.1.10", + "globby": "^13.2.2", + "isbinaryfile": "5.0.2", + "minimatch": "^9.0.3", + "multimatch": "^6.0.0", + "normalize-path": "^3.0.0", + "textextensions": "^5.16.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "mem-fs": "^4.0.0" + } + }, + "node_modules/mem-fs-editor/node_modules/@types/node": { + "version": "18.19.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz", + "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/mem-fs-editor/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mem-fs-editor/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-jsons-webpack-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/merge-jsons-webpack-plugin/-/merge-jsons-webpack-plugin-2.0.1.tgz", + "integrity": "sha512-8GP8rpOX3HSFsm7Gx+b3OAQR7yhgeAQvMqcZOJ+/cQIrqdak1c42a2T2vyeee8pzGPBf7pMLumthPh4CHgv2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "7.1.1" + } + }, + "node_modules/merge-jsons-webpack-plugin/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/merge-jsons-webpack-plugin/node_modules/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-mRyN/EsN2SyNhKWykF3eEGhDpeNplMWaW18Bmh76tnOqk5TbELAVwFAYOCmKVssOYFrYvvLMguiA+NXO3ZTuVA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/merge-jsons-webpack-plugin/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-json-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", + "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multimatch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-6.0.0.tgz", + "integrity": "sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.5", + "array-differ": "^4.0.0", + "array-union": "^3.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ng2-charts": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-3.1.2.tgz", + "integrity": "sha512-KkbpQhbf8DhcEm5SnqKBQMa25cBOKBiopjl4+8aUbfOaKY59uOfImAYPDsT3du7cfMVD6w7xt4SWhRbbelY9sQ==", + "dependencies": { + "lodash-es": "^4.17.15", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=11.0.0", + "@angular/core": ">=11.0.0", + "chart.js": "^3.4.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/ngx-color-picker": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-17.0.0.tgz", + "integrity": "sha512-kHuhW4vErpb0LlBlgTnf1+cYWdaq0gOvDwiX9LeaFKNvhV5li+YEyk7tC3o1Sbhqd4lsFKb8zHyqi1teLWN4Zg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0", + "@angular/core": ">=9.0.0", + "@angular/forms": ">=9.0.0" + } + }, + "node_modules/ngx-infinite-scroll": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-17.0.0.tgz", + "integrity": "sha512-pQXLuRiuhRuDKD3nmgyW1V08JVNBepmk6nb8qjHc5hgsWNts01+R/p33rYcRDzcut6/PWqGyrZ9o9i8swzMYMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=17.0.0 <18.0.0", + "@angular/core": ">=17.0.0 <18.0.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/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==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/node-notifier": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-9.0.1.tgz", + "integrity": "sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", + "shellwords": "^0.1.1", + "uuid": "^8.3.0", + "which": "^2.0.2" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", + "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", + "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^1.1.0", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT", + "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-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-locale": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-6.0.2.tgz", + "integrity": "sha512-qIb8bzRqaN/vVqEYZ7lTAg6PonskO7xOmM7OClD28F6eFa4s5XGe4bGpHUHMoCHbNNuR0pDYFeSLiW5bnjWXIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lcid": "^3.1.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/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, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.4.1.tgz", + "integrity": "sha512-vRpMXmIkYF2/1hLBKisKeVYJZ8S2tZ0zEAmIJgdVKP2nq0nh4qCdf8bgw+ZgKrkh71AOCaqzwbJJk1WtdcF3VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^5.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", + "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-transform": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/p-transform/-/p-transform-4.1.5.tgz", + "integrity": "sha512-CsXIiCOeBUYMBLpcY71DTq+fg8268ux31pAxI5TcoYEPfWCw5ozrbgWdZ9QmSDd8dUzvNXtmiwJOdTIxIFptfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^16.18.31", + "p-queue": "^7.3.0", + "readable-stream": "^4.3.0" + }, + "engines": { + "node": ">=16.13.0" + } + }, + "node_modules/p-transform/node_modules/@types/node": { + "version": "16.18.113", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.113.tgz", + "integrity": "sha512-4jHxcEzSXpF1cBNxogs5FVbVSFSKo50sFCn7Xg7vmjJTbWFWgeuHW3QnoINlfmfG++MFR/q97RZE5RQXKeT+jg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", + "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ky": "^1.2.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.6.tgz", + "integrity": "sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^7.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-conflict-json": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz", + "integrity": "sha512-01TvEktc68vwbJOtWZluyWeVGWjP+bZwXtPDMQVbBKzbJ/vZBif0L69KH1+cHv1SZ6e0FKLvjyHe8mqsIqYOmw==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/parse-gitignore": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", + "integrity": "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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, + "license": "MIT", + "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/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", + "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/portscanner/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-java": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.6.0.tgz", + "integrity": "sha512-mHZ3Ub3WAyYSUe1mMbiGH85xYV+NtzJgNsrfLNYDKvL7NfvoKBuJiEW4Xa2MFG668f9uRdj38WEuPKmRu+nv/g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "java-parser": "2.3.0", + "lodash": "4.17.21", + "prettier": "3.2.5" + } + }, + "node_modules/prettier-plugin-java/node_modules/java-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-2.3.0.tgz", + "integrity": "sha512-P6Ma4LU1w/e0Lr4SVM/0PtqCGoL2/i/KP9ZoiyLa824oBqhF0yGTgHDyZkLgp9GTzqR43wm5wabE56FF5X7cqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chevrotain": "11.0.3", + "chevrotain-allstar": "0.3.1", + "lodash": "4.17.21" + } + }, + "node_modules/prettier-plugin-packagejson": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.0.tgz", + "integrity": "sha512-6XkH3rpin5QEQodBSVNg+rBo4r91g/1mCaRwS1YGdQJZ6jwqrg2UchBsIG9tpS1yK1kNBvOt84OILsX8uHzBGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-package-json": "2.10.0", + "synckit": "0.9.0" + }, + "peerDependencies": { + "prettier": ">= 1.16.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/prettier-plugin-properties": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-properties/-/prettier-plugin-properties-0.3.0.tgz", + "integrity": "sha512-j2h4NbG6hIaRx0W7CDPUH+yDbzXHmI2urPC1EC8pYrsS5R5AYgAmcSDN0oea+C5mBiN6BK0AkiSKPj0RC17NDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-properties": "^1.0.0" + }, + "peerDependencies": { + "prettier": ">= 2.3.0" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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/pretty-format/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proggy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/proggy/-/proggy-2.0.0.tgz", + "integrity": "sha512-69agxLtnI8xBs9gUGqEnK26UfiexpHy+KUpBQWabiytQjnn5wFY8rklAi7GRfABIuPNnQ/ik48+LGLkYYJcy4A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/promise-all-reject-late": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", + "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promise-call-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", + "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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, + "license": "MIT" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quill-better-table": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/quill-better-table/-/quill-better-table-1.2.10.tgz", + "integrity": "sha512-CFwxAQzt4EPCQuynQ65R/FU7Yu//kcDBb/rmBBOsFfO758+q50zvG/PDt4Lenv9DcrSgwnyNkfo4yeA5fzzVYQ==" + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "dev": true, + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-cmd-shim": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", + "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", + "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", + "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "index-to-position": "^0.1.2", + "type-fest": "^4.7.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", + "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/resp-modifier/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/resp-modifier/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/resp-modifier/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/resp-modifier/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", + "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "dev": true, + "license": "Apache-2.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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==", + "license": "Apache-2.0" + }, + "node_modules/sass": { + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/send/node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/send/node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/simple-git": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.24.0.tgz", + "integrity": "sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-object-keys": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz", + "integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sort-package-json": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.10.0.tgz", + "integrity": "sha512-MYecfvObMwJjjJskhxYfuOADkXp1ZMMnCFC8yhp+9HDsk7HhR336hd7eiBs96lTXfiqmUNI+WQCeCMRBhl251g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-indent": "^7.0.1", + "detect-newline": "^4.0.0", + "get-stdin": "^9.0.0", + "git-hooks-list": "^3.0.0", + "globby": "^13.1.2", + "is-plain-obj": "^4.1.0", + "semver": "^7.6.0", + "sort-object-keys": "^1.1.3" + }, + "bin": { + "sort-package-json": "cli.js" + } + }, + "node_modules/sort-package-json/node_modules/detect-newline": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz", + "integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-package-json/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-package-json/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stdin-discarder/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/stdin-discarder/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-throttle": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^2.2.0", + "limiter": "^1.0.5" + }, + "bin": { + "throttleproxy": "bin/throttleproxy.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-throttle/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.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, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-buf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-3.0.1.tgz", + "integrity": "sha512-iJaWw2WroigLHzQysdc5WWeUc99p7ea7AEgB6JkY8CMyiO1yTVAA1gIlJJgORElUIR+lcZJkNl1OGChMhvc2Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-bom-stream": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-5.0.0.tgz", + "integrity": "sha512-Yo472mU+3smhzqeKlIxClre4s4pwtYZEvDNQvY/sJpnChdaxmKuwU28UVx/v1ORKNMxkmj1GBuvxJQyBk6wYMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "first-chunk-stream": "^5.0.0", + "strip-bom-buf": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stubborn-fs": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", + "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==", + "dev": true + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.0.tgz", + "integrity": "sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/test-exclude/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz", + "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true, + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-5.16.0.tgz", + "integrity": "sha512-7D/r3s6uPZyU//MCYrX6I14nzauDwJ5CxazouuRGNuvSCihW87ufN6VLoROLCrHg6FblLuJrT6N2BVaPVzqElw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinymce": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.8.0.tgz", + "integrity": "sha512-MUER5MWV9mkOB4expgbWknh/C5ZJvOXQlMVSx4tJxTuYtcUCDB6bMZ34fWNOIc8LvrnXmGHGj0eGQuxjQyRgrA==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "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", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/treeverse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", + "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.1.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", + "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", + "dev": true, + "license": "MIT", + "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": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.3.tgz", + "integrity": "sha512-Q08/0IrpvM+NMY9PA2rti9Jb+JejTddwmwmVQGskAlhtcrw1wsRzoR6ode6mR+OAabNa75w/dxedSUY2mlphaQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/uint8array-extras": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-0.3.0.tgz", + "integrity": "sha512-erJsJwQ0tKdwuqI0359U8ijkFmfiTcq25JvvzRVc1VP+2son1NJRXhxcAKJmAW3ajM8JSGAfsAXye8g4s+znxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", + "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-5.0.0.tgz", + "integrity": "sha512-bOgQLUnd2G5rhzaTvh1VCI9Fo6bC5cLTpH17T5aFfamyXFYDbbdzN6IXdeoc3jBS7T9hNTmJtYUzJCJ2Xlc9gA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "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/v8-to-istanbul/node_modules/convert-source-map": { + "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, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-5.0.0.tgz", + "integrity": "sha512-MvkPF/yA1EX7c6p+juVIvp9+Lxp70YUfNKzEWeHMKpUNVSnTZh2coaOqLxI0pmOe2V9nB+OkgFaMDkodaJUyGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/vinyl": "^2.0.7", + "strip-bom-buf": "^3.0.1", + "strip-bom-stream": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", + "integrity": "sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/walk-up-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", + "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.91.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", + "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.16.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/webpack-dev-server/node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/webpack-dev-server/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-notifier": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/webpack-notifier/-/webpack-notifier-1.15.0.tgz", + "integrity": "sha512-N2V8UMgRB5komdXQRavBsRpw0hPhJq2/SWNOGuhrXpIgRhcMexzkGQysUyGStHLV5hkUlgpRiF7IUXoBqyMmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "node-notifier": "^9.0.0", + "strip-ansi": "^6.0.0" + }, + "peerDependencies": { + "@types/webpack": ">4.41.31" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack/node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/when-exit": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.3.tgz", + "integrity": "sha512-uVieSTccFIr/SFQdFWN/fFaQYmV37OKtuaGphMAzi4DmmUlrvRBJW5WSLkHyjNQY/ePJMz3LoiX9R3yy1Su6Hw==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-package-manager": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/which-package-manager/-/which-package-manager-0.0.1.tgz", + "integrity": "sha512-a+bCExXd8OdYky5J59nimHxTCRPhxZSQtwKh3Ew6lpC4oY9f3KH77XDxcPrComVhSEPtvMjZigS2vZgZfgJuxA==", + "dev": true, + "dependencies": { + "execa": "^7.1.1", + "find-up": "^6.3.0", + "micromatch": "^4.0.5" + }, + "engines": { + "node": "^16.13.0 || >=18.12.0" + } + }, + "node_modules/which-package-manager/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/which-package-manager/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/which-package-manager/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/which-package-manager/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, + "license": "ISC" + }, + "node_modules/which-package-manager/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-package-manager/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/winston": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", + "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.8.0.tgz", + "integrity": "sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^4.5.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "license": "MIT", + "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/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "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, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/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, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "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/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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yeoman-environment": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-4.4.0.tgz", + "integrity": "sha512-X53IlyzOI4gX1vEGlxW1Q9lJ73sJq4o13gLeDw5IzDMBVn0xnmsmxphs+dWDzSmipirmZQaWKQhBH/rEAXTCLA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@yeoman/adapter": "^1.4.0", + "@yeoman/conflicter": "^2.0.0-alpha.2", + "@yeoman/namespace": "^1.0.0", + "@yeoman/transform": "^1.2.0", + "@yeoman/types": "^1.1.1", + "arrify": "^3.0.0", + "chalk": "^5.3.0", + "commander": "^11.1.0", + "debug": "^4.3.4", + "execa": "^8.0.1", + "fly-import": "^0.4.0", + "globby": "^14.0.0", + "grouped-queue": "^2.0.0", + "locate-path": "^7.2.0", + "lodash-es": "^4.17.21", + "mem-fs": "^4.0.0", + "mem-fs-editor": "^11.0.0", + "semver": "^7.5.4", + "slash": "^5.1.0", + "untildify": "^5.0.0", + "which-package-manager": "^0.0.1" + }, + "bin": { + "yoe": "bin/bin.cjs" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "peerDependencies": { + "@yeoman/types": "^1.1.1", + "mem-fs": "^4.0.0" + } + }, + "node_modules/yeoman-environment/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/yeoman-environment/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/yeoman-environment/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/yeoman-environment/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/yeoman-environment/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-environment/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-generator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-7.2.0.tgz", + "integrity": "sha512-z+iu9PZuE/Nvj1MVuQdMPIIknHUoigNiVXd20DX+ImIvdehNeBfxUSuqQPVdGu/D1H/b5YnNW5Ji9iGJOU2TgQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/lodash-es": "^4.17.9", + "@types/node": "^18.18.5", + "@yeoman/namespace": "^1.0.0", + "chalk": "^5.3.0", + "debug": "^4.1.1", + "execa": "^8.0.1", + "github-username": "^7.0.0", + "json-schema": "^0.4.0", + "latest-version": "^7.0.0", + "lodash-es": "^4.17.21", + "mem-fs-editor": "^11.0.1", + "minimist": "^1.2.8", + "read-package-up": "^11.0.0", + "semver": "^7.5.4", + "simple-git": "^3.20.0", + "sort-keys": "^5.0.0", + "text-table": "^0.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "peerDependencies": { + "@yeoman/types": "^1.1.1", + "mem-fs": "^4.0.0", + "yeoman-environment": "^4.0.0" + }, + "peerDependenciesMeta": { + "yeoman-environment": { + "optional": true + } + } + }, + "node_modules/yeoman-generator/node_modules/@types/node": { + "version": "18.19.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz", + "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/yeoman-generator/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/yeoman-generator/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/yeoman-generator/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-generator/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/yeoman-generator/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-generator/node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-generator/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-generator/node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yeoman-generator/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.6.tgz", + "integrity": "sha512-vyRNFqofdaHVdWAy7v3Bzmn84a1JHWSjpuTZROT/uYn8I3p2cmo7Ro9twFmYRQDPhiYOV7QLk0hhY4JJQVqS6Q==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8b18fa8 --- /dev/null +++ b/package.json @@ -0,0 +1,162 @@ +{ + "name": "resilient", + "version": "0.0.1-SNAPSHOT", + "private": true, + "description": "Description for Resilient", + "license": "UNLICENSED", + "scripts": { + "app:start": "./mvnw", + "app:up": "docker compose -f src/main/docker/app.yml up --wait", + "backend:build-cache": "./mvnw dependency:go-offline -ntp", + "backend:debug": "./mvnw -Dspring-boot.run.jvmArguments=\"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000\"", + "backend:doc:test": "./mvnw -ntp javadoc:javadoc --batch-mode", + "backend:info": "./mvnw --version", + "backend:nohttp:test": "./mvnw -ntp checkstyle:check --batch-mode", + "backend:start": "./mvnw -Dskip.installnodenpm -Dskip.npm", + "backend:unit:test": "./mvnw -ntp -Dskip.installnodenpm -Dskip.npm verify --batch-mode -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.oguerreiro.resilient=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF", + "build": "npm run webapp:prod --", + "build-watch": "concurrently 'npm run webapp:build:dev -- --watch' npm:backend:start", + "ci:backend:test": "npm run backend:info && npm run backend:doc:test && npm run backend:nohttp:test && npm run backend:unit:test -- -P$npm_package_config_default_environment", + "ci:e2e:package": "npm run java:$npm_package_config_packaging:$npm_package_config_default_environment -- -Pe2e -Denforcer.skip=true", + "ci:e2e:prepare": "npm run ci:e2e:prepare:docker", + "ci:e2e:prepare:docker": "npm run services:up --if-present && docker ps -a", + "preci:e2e:server:start": "npm run services:db:await --if-present && npm run services:others:await --if-present", + "ci:e2e:server:start": "java -jar target/e2e.$npm_package_config_packaging --spring.profiles.active=e2e,$npm_package_config_default_environment -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.oguerreiro.resilient=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF --logging.level.org.springframework.web=ERROR", + "ci:e2e:teardown": "npm run ci:e2e:teardown:docker --if-present", + "ci:e2e:teardown:docker": "docker compose -f src/main/docker/services.yml down -v && docker ps -a", + "ci:frontend:build": "npm run webapp:build:$npm_package_config_default_environment", + "ci:frontend:test": "npm run ci:frontend:build && npm test", + "clean-www": "rimraf target/classes/static/", + "cleanup": "rimraf target/", + "docker:db:down": "docker compose -f src/main/docker/mysql.yml down -v", + "docker:db:up": "docker compose -f src/main/docker/mysql.yml up --wait", + "java:docker": "./mvnw -ntp verify -DskipTests -Pprod jib:dockerBuild", + "java:docker:arm64": "npm run java:docker -- -Djib-maven-plugin.architecture=arm64", + "java:docker:dev": "npm run java:docker -- -Pdev,webapp", + "java:docker:prod": "npm run java:docker -- -Pprod", + "java:jar": "./mvnw -ntp verify -DskipTests --batch-mode", + "java:jar:dev": "npm run java:jar -- -Pdev,webapp", + "java:jar:prod": "npm run java:jar -- -Pprod", + "java:war": "./mvnw -ntp verify -DskipTests --batch-mode -Pwar", + "java:war:dev": "npm run java:war -- -Pdev,webapp", + "java:war:prod": "npm run java:war -- -Pprod", + "jest": "jest --coverage --logHeapUsage --maxWorkers=2 --config jest.conf.js", + "lint": "eslint . --ext .js,.ts", + "lint:fix": "npm run lint -- --fix", + "prepare": "husky", + "prettier:check": "prettier --check \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "prettier:format": "prettier --write \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "serve": "npm run start --", + "services:up": "docker compose -f src/main/docker/services.yml up --wait", + "start": "ng serve --hmr", + "start-tls": "npm run webapp:dev-ssl", + "pretest": "npm run lint", + "test": "ng test --coverage --log-heap-usage -w=2", + "test:watch": "npm run test -- --watch", + "watch": "concurrently npm:start npm:backend:start", + "webapp:build": "npm run clean-www && npm run webapp:build:dev", + "webapp:build:dev": "ng build --configuration development", + "webapp:build:prod": "ng build --configuration production", + "webapp:dev": "ng serve", + "webapp:dev-ssl": "ng serve --ssl", + "webapp:dev-verbose": "ng serve --verbose", + "webapp:prod": "npm run clean-www && npm run webapp:build:prod", + "webapp:test": "npm run test --" + }, + "config": { + "backend_port": "8081", + "default_environment": "prod", + "packaging": "jar" + }, + "dependencies": { + "@angular/animations": "17.3.9", + "@angular/common": "17.3.9", + "@angular/compiler": "17.3.9", + "@angular/core": "17.3.9", + "@angular/forms": "17.3.9", + "@angular/localize": "17.3.9", + "@angular/material": "17.3.9", + "@angular/platform-browser": "17.3.9", + "@angular/platform-browser-dynamic": "17.3.9", + "@angular/router": "17.3.9", + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/lang-java": "^6.0.1", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.34.3", + "@fortawesome/angular-fontawesome": "0.14.1", + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@ng-bootstrap/ng-bootstrap": "16.0.0", + "@ng-select/ng-select": "^12.0.0", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", + "@popperjs/core": "2.11.8", + "@tinymce/tinymce-angular": "^8.0.1", + "@types/codemirror": "^5.60.15", + "bootstrap": "5.3.3", + "chart.js": "^3.9.1", + "codemirror": "^6.0.1", + "dayjs": "1.11.11", + "ng2-charts": "^3.1.2", + "ngx-color-picker": "^17.0.0", + "ngx-infinite-scroll": "17.0.0", + "quill-better-table": "^1.2.10", + "rxjs": "7.8.1", + "tslib": "2.6.2", + "zone.js": "0.14.6" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "17.0.2", + "@angular-builders/jest": "17.0.3", + "@angular-devkit/build-angular": "17.3.7", + "@angular-eslint/eslint-plugin": "17.5.2", + "@angular/cli": "17.3.7", + "@angular/compiler-cli": "17.3.9", + "@angular/service-worker": "17.3.9", + "@types/jest": "29.5.12", + "@types/node": "20.11.25", + "@types/quill": "^2.0.14", + "@typescript-eslint/eslint-plugin": "7.11.0", + "@typescript-eslint/parser": "7.11.0", + "browser-sync": "3.0.2", + "browser-sync-webpack-plugin": "2.3.0", + "buffer": "6.0.3", + "concurrently": "8.2.2", + "copy-webpack-plugin": "12.0.2", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-webpack-plugin": "4.2.0", + "folder-hash": "4.0.4", + "generator-jhipster": "8.5.0", + "husky": "9.0.11", + "jest": "29.7.0", + "jest-date-mock": "1.0.10", + "jest-environment-jsdom": "29.7.0", + "jest-junit": "16.0.0", + "jest-preset-angular": "14.1.0", + "jest-sonar": "0.2.16", + "lint-staged": "15.2.5", + "merge-jsons-webpack-plugin": "2.0.1", + "prettier": "3.2.5", + "prettier-plugin-java": "2.6.0", + "prettier-plugin-packagejson": "2.5.0", + "rimraf": "5.0.7", + "swagger-ui-dist": "5.17.14", + "ts-jest": "29.1.4", + "typescript": "5.4.5", + "wait-on": "7.2.0", + "webpack-bundle-analyzer": "4.10.2", + "webpack-merge": "5.10.0", + "webpack-notifier": "1.15.0" + }, + "engines": { + "node": ">=20.14.0" + }, + "cacheDirectories": [ + "node_modules" + ], + "overrides": { + "browser-sync": "3.0.2", + "webpack": "5.91.0" + } +} diff --git a/package.json.old b/package.json.old new file mode 100644 index 0000000..efc00a1 --- /dev/null +++ b/package.json.old @@ -0,0 +1,147 @@ +{ + "name": "resilient", + "version": "0.0.1-SNAPSHOT", + "private": true, + "description": "Description for Resilient", + "license": "UNLICENSED", + "scripts": { + "app:start": "./mvnw", + "app:up": "docker compose -f src/main/docker/app.yml up --wait", + "backend:build-cache": "./mvnw dependency:go-offline -ntp", + "backend:debug": "./mvnw -Dspring-boot.run.jvmArguments=\"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000\"", + "backend:doc:test": "./mvnw -ntp javadoc:javadoc --batch-mode", + "backend:info": "./mvnw --version", + "backend:nohttp:test": "./mvnw -ntp checkstyle:check --batch-mode", + "backend:start": "./mvnw -Dskip.installnodenpm -Dskip.npm", + "backend:unit:test": "./mvnw -ntp -Dskip.installnodenpm -Dskip.npm verify --batch-mode -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.oguerreiro.resilient=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF", + "build": "npm run webapp:prod --", + "build-watch": "concurrently 'npm run webapp:build:dev -- --watch' npm:backend:start", + "ci:backend:test": "npm run backend:info && npm run backend:doc:test && npm run backend:nohttp:test && npm run backend:unit:test -- -P$npm_package_config_default_environment", + "ci:e2e:package": "npm run java:$npm_package_config_packaging:$npm_package_config_default_environment -- -Pe2e -Denforcer.skip=true", + "ci:e2e:prepare": "npm run ci:e2e:prepare:docker", + "ci:e2e:prepare:docker": "npm run services:up --if-present && docker ps -a", + "preci:e2e:server:start": "npm run services:db:await --if-present && npm run services:others:await --if-present", + "ci:e2e:server:start": "java -jar target/e2e.$npm_package_config_packaging --spring.profiles.active=e2e,$npm_package_config_default_environment -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.oguerreiro.resilient=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF --logging.level.org.springframework.web=ERROR", + "ci:e2e:teardown": "npm run ci:e2e:teardown:docker --if-present", + "ci:e2e:teardown:docker": "docker compose -f src/main/docker/services.yml down -v && docker ps -a", + "ci:frontend:build": "npm run webapp:build:$npm_package_config_default_environment", + "ci:frontend:test": "npm run ci:frontend:build && npm test", + "clean-www": "rimraf target/classes/static/", + "cleanup": "rimraf target/", + "docker:db:down": "docker compose -f src/main/docker/mysql.yml down -v", + "docker:db:up": "docker compose -f src/main/docker/mysql.yml up --wait", + "java:docker": "./mvnw -ntp verify -DskipTests -Pprod jib:dockerBuild", + "java:docker:arm64": "npm run java:docker -- -Djib-maven-plugin.architecture=arm64", + "java:docker:dev": "npm run java:docker -- -Pdev,webapp", + "java:docker:prod": "npm run java:docker -- -Pprod", + "java:jar": "./mvnw -ntp verify -DskipTests --batch-mode", + "java:jar:dev": "npm run java:jar -- -Pdev,webapp", + "java:jar:prod": "npm run java:jar -- -Pprod", + "java:war": "./mvnw -ntp verify -DskipTests --batch-mode -Pwar", + "java:war:dev": "npm run java:war -- -Pdev,webapp", + "java:war:prod": "npm run java:war -- -Pprod", + "jest": "jest --coverage --logHeapUsage --maxWorkers=2 --config jest.conf.js", + "lint": "eslint . --ext .js,.ts", + "lint:fix": "npm run lint -- --fix", + "prepare": "husky", + "prettier:check": "prettier --check \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "prettier:format": "prettier --write \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "serve": "npm run start --", + "services:up": "docker compose -f src/main/docker/services.yml up --wait", + "start": "ng serve --hmr", + "start-tls": "npm run webapp:dev-ssl", + "pretest": "npm run lint", + "test": "ng test --coverage --log-heap-usage -w=2", + "test:watch": "npm run test -- --watch", + "watch": "concurrently npm:start npm:backend:start", + "webapp:build": "npm run clean-www && npm run webapp:build:dev", + "webapp:build:dev": "ng build --configuration development", + "webapp:build:prod": "ng build --configuration production", + "webapp:dev": "ng serve", + "webapp:dev-ssl": "ng serve --ssl", + "webapp:dev-verbose": "ng serve --verbose", + "webapp:prod": "npm run clean-www && npm run webapp:build:prod", + "webapp:test": "npm run test --" + }, + "config": { + "backend_port": "8081", + "default_environment": "prod", + "packaging": "jar" + }, + "dependencies": { + "@angular/common": "17.3.9", + "@angular/compiler": "17.3.9", + "@angular/core": "17.3.9", + "@angular/forms": "17.3.9", + "@angular/localize": "17.3.9", + "@angular/platform-browser": "17.3.9", + "@angular/platform-browser-dynamic": "17.3.9", + "@angular/router": "17.3.9", + "@fortawesome/angular-fontawesome": "0.14.1", + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@ng-bootstrap/ng-bootstrap": "16.0.0", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", + "@popperjs/core": "2.11.8", + "bootstrap": "5.3.3", + "dayjs": "1.11.11", + "ngx-infinite-scroll": "17.0.0", + "rxjs": "7.8.1", + "tslib": "2.6.2", + "zone.js": "0.14.6" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "17.0.2", + "@angular-builders/jest": "17.0.3", + "@angular-devkit/build-angular": "17.3.7", + "@angular-eslint/eslint-plugin": "17.5.2", + "@angular/cli": "17.3.7", + "@angular/compiler-cli": "17.3.9", + "@angular/service-worker": "17.3.9", + "@types/jest": "29.5.12", + "@types/node": "20.11.25", + "@typescript-eslint/eslint-plugin": "7.11.0", + "@typescript-eslint/parser": "7.11.0", + "browser-sync": "3.0.2", + "browser-sync-webpack-plugin": "2.3.0", + "buffer": "6.0.3", + "concurrently": "8.2.2", + "copy-webpack-plugin": "12.0.2", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-webpack-plugin": "4.2.0", + "folder-hash": "4.0.4", + "generator-jhipster": "8.5.0", + "husky": "9.0.11", + "jest": "29.7.0", + "jest-date-mock": "1.0.10", + "jest-environment-jsdom": "29.7.0", + "jest-junit": "16.0.0", + "jest-preset-angular": "14.1.0", + "jest-sonar": "0.2.16", + "lint-staged": "15.2.5", + "merge-jsons-webpack-plugin": "2.0.1", + "prettier": "3.2.5", + "prettier-plugin-java": "2.6.0", + "prettier-plugin-packagejson": "2.5.0", + "rimraf": "5.0.7", + "swagger-ui-dist": "5.17.14", + "ts-jest": "29.1.4", + "typescript": "5.4.5", + "wait-on": "7.2.0", + "webpack-bundle-analyzer": "4.10.2", + "webpack-merge": "5.10.0", + "webpack-notifier": "1.15.0" + }, + "engines": { + "node": ">=20.14.0" + }, + "cacheDirectories": [ + "node_modules" + ], + "overrides": { + "browser-sync": "3.0.2", + "webpack": "5.91.0" + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a89f70c --- /dev/null +++ b/pom.xml @@ -0,0 +1,1291 @@ + + + + 4.0.0 + + com.oguerreiro.resilient + resilient + 1.0.0 + jar + Resilient + Description for Resilient + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + + + + + + ${project.version} + 3.2.5 + 17 + v20.14.0 + 10.8.1 + UTF-8 + UTF-8 + yyyyMMddHHmmss + ${java.version} + ${java.version} + com.oguerreiro.resilient.ResilientApp + -Djava.security.egd=file:/dev/./urandom -Xmx1G + jdt_apt + false + ${project.parent.version} + 1.3.0 + 10.17.0 + 1.11 + 1.15.0 + 8.0.2 + 0.8.12 + 8.5.0 + amd64 + eclipse-temurin:17-jre-focal + 3.4.2 + 1.0.0 + + + + 1.5.5.Final + 3.1.0 + 3.3.1 + 3.3.2 + 3.13.0 + 3.5.0 + 3.2.5 + 3.4.1 + 3.6.3 + 3.3.1 + 3.12.1 + 3.2.5 + 3.4.0 + 2.9.0 + 0.0.11 + + + + + + 1.2.1 + 3.11.0.3922 + 2.43.0 + 2.5.0 + 5.2.5 + 2.4.12.Final + + + + + + + org.checkerframework + checker-qual + 3.37.0 + + + + + com.google.errorprone + error_prone_annotations + 2.21.1 + + + + + org.bouncycastle + bcprov-jdk15to18 + 1.71 + + + org.bouncycastle + bcpkix-jdk15to18 + 1.71 + + + org.bouncycastle + bcutil-jdk15to18 + 1.71 + + + + + + + tech.jhipster + jhipster-framework + ${jhipster-framework.version} + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + org.springframework.boot + spring-boot-loader-tools + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-test + test + + + org.springframework.security + spring-security-data + + + org.springframework.security + spring-security-test + test + + + + + org.springframework.security + spring-security-saml2-service-provider + 6.3.0 + + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + + + org.bouncycastle + bcutil-jdk18on + + + + + + + org.bouncycastle + bcprov-jdk15to18 + + + org.bouncycastle + bcpkix-jdk15to18 + + + org.bouncycastle + bcutil-jdk15to18 + + + + org.springdoc + springdoc-openapi-starter-webmvc-api + ${springdoc-openapi-starter-webmvc-api.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate6 + + + com.fasterxml.jackson.datatype + jackson-datatype-hppc + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + + + + com.mysql + mysql-connector-j + + + org.mariadb.jdbc + mariadb-java-client + + + + com.tngtech.archunit + archunit-junit5-api + ${archunit-junit5.version} + test + + + + + com.tngtech.archunit + archunit-junit5-engine + ${archunit-junit5.version} + test + + + com.zaxxer + HikariCP + + + io.micrometer + micrometer-registry-prometheus-simpleclient + + + jakarta.annotation + jakarta.annotation-api + + + + jakarta.data + jakarta.data-api + 1.0.1 + + + javax.cache + cache-api + + + org.apache.commons + commons-lang3 + + + org.ehcache + ehcache + jakarta + + + org.glassfish.jaxb + jaxb-runtime + provided + + + org.hibernate.orm + hibernate-core + + + org.hibernate.orm + hibernate-jcache + + + org.hibernate.orm + hibernate-jpamodelgen + provided + + + org.hibernate.validator + hibernate-validator + + + org.liquibase + liquibase-core + ${liquibase.version} + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + + org.apache.poi + poi + ${apache-poi.version} + + + org.apache.poi + poi-ooxml + ${apache-poi.version} + + + + org.apache.commons + commons-compress + 1.25.0 + + + + org.mvel + mvel2 + ${mvel.version} + + + org.testcontainers + jdbc + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mysql + test + + + org.testcontainers + testcontainers + test + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + + + com.diffplug.spotless + spotless-maven-plugin + + + com.google.cloud.tools + jib-maven-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.codehaus.mojo + properties-maven-plugin + + + org.gaul + modernizer-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + org.sonarsource.scanner.maven + sonar-maven-plugin + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + ${start-class} + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + + + + + + spotless + process-sources + + apply + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + target + ${node.version} + ${npm.version} + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${jib-maven-plugin.image} + + + ${jib-maven-plugin.architecture} + linux + + + + + resilient:latest + + + + bash + + /entrypoint.sh + + + 8081 + + + ALWAYS + 0 + + USE_CURRENT_TIMESTAMP + 1000 + + + src/main/docker/jib + + + /entrypoint.sh + 755 + + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + ${git-commit-id-maven-plugin.version} + + + + revision + + + + + false + false + true + + ^git.commit.id.abbrev$ + ^git.commit.id.describe$ + ^git.branch$ + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + ${checksum-maven-plugin.version} + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven-antrun-plugin.version} + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + io.spring.nohttp + nohttp-checkstyle + ${nohttp-checkstyle.version} + + + + checkstyle.xml + pom.xml,README.md + .git/**/*,target/**/*,node_modules/**/* + ./ + + + + + check + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + true + + + org.springframework.boot + spring-boot-configuration-processor + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.hibernate.orm + hibernate-jpamodelgen + + + org.glassfish.jaxb + jaxb-runtime + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-versions + + enforce + + + + enforce-dependencyConvergence + + + + + false + + + enforce + + + + + + + You are running an older version of Maven. JHipster requires at least Maven ${maven.version} + [${maven.version},) + + + You are running an incompatible version of Java. JHipster supports JDK 17 to 21. + [17,18),[18,19),[19,20),[20,21),[21,22) + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + + ${project.build.outputDirectory} + alphabetical + + **/*IT* + **/*IntTest* + + @{argLine} -Dspring.profiles.active=${profile.test} + + + + integration-test + + integration-test + + + + verify + + verify + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + ${maven.compiler.source} + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + config-resources + validate + + copy-resources + + + ${project.build.directory}/classes + + + src/main/resources/ + true + + config/*.yml + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + alphabetical + + **/*IT* + **/*IntTest* + + + src/test/resources/logback.xml + + + + + org.apache.maven.plugins + maven-war-plugin + ${maven-war-plugin.version} + + + default-war + + war + + package + + + + WEB-INF/**,META-INF/** + false + target/classes/static/ + + + src/main/webapp + + WEB-INF/** + + + + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-maven-plugin.version} + + + initialize + + read-project-properties + + + + sonar-project.properties + + + + + + + org.gaul + modernizer-maven-plugin + ${modernizer-maven-plugin.version} + + + modernizer + package + + modernizer + + + + + ${java.version} + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + pre-unit-tests + + prepare-agent + + + + + post-unit-test + test + + report + + + + pre-integration-tests + + prepare-agent-integration + + + + + post-integration-tests + post-integration-test + + report-integration + + + + + + org.liquibase + liquibase-maven-plugin + ${liquibase.version} + + config/liquibase/master.xml + ${project.basedir}/src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml + com.mysql.cj.jdbc.Driver + ${liquibase-plugin.url} + + ${liquibase-plugin.username} + ${liquibase-plugin.password} + hibernate:spring:com.oguerreiro.resilient.domain?dialect=org.hibernate.dialect.MySQL8Dialect&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + true + debug + !test + + + + org.liquibase + liquibase-core + ${liquibase.version} + + + org.liquibase.ext + liquibase-hibernate6 + ${liquibase.version} + + + jakarta.validation + jakarta.validation-api + ${jakarta-validation.version} + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring-boot.version} + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + + + + + + src/main/resources + true + + application.yml + + + + + + src/main/resources + false + + application.yml + + + + + + + + + api-docs + + ,api-docs + + + + dev + + true + + + + dev${profile.tls}${profile.no-liquibase} + testdev + jdbc:mysql://localhost:3306/resilient + root + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + + eclipse + + + m2e.version + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + + org.eclipse.m2e + lifecycle-mapping + ${lifecycle-mapping.version} + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + prepare-agent + + + + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + install-node-and-npm + npm + + + + + + + + + + + + + + + + + IDE + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.hibernate.orm + hibernate-jpamodelgen + + + + + no-liquibase + + ,no-liquibase + + + + prod + + + prod${profile.api-docs}${profile.tls}${profile.e2e}${profile.no-liquibase} + testprod + jdbc:mysql://localhost:3306/resilient + root + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + target/classes/static/ + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build test + + npm + + test + + run webapp:test + false + + + + webapp build prod + + npm + + generate-resources + + run webapp:prod + + ${project.version} + + false + + + + + + + + + tls + + ,tls + + + + war + + + + org.apache.maven.plugins + maven-war-plugin + + + + + + webapp + + true + + + + dev${profile.no-liquibase} + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + + + create-pre-compiled-webapp-checksum + + files + + generate-resources + + + create-compiled-webapp-checksum + + files + + compile + + checksums.csv.old + + + + + + + ${project.basedir} + + src/main/webapp/**/*.* + target/classes/static/**/*.* + package-lock.json + package.json + tsconfig.json + tsconfig.app.json + webpack/*.* + + + **/app/**/service-worker.js + **/app/**/vendor.css + + + + false + false + false + + SHA-1 + + true + true + + + + org.apache.maven.plugins + maven-antrun-plugin + + + eval-frontend-checksum + generate-resources + + run + + + + + + + + + + + + true + + + + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build dev + + npm + + generate-resources + + run webapp:build + + ${project.version} + + false + + + + + + + + + + + + + shibboleth + https://build.shibboleth.net/maven/releases/ + + + \ No newline at end of file diff --git a/pom.xml.old b/pom.xml.old new file mode 100644 index 0000000..9bb6578 --- /dev/null +++ b/pom.xml.old @@ -0,0 +1,1152 @@ + + + 4.0.0 + + com.oguerreiro.resilient + resilient + 0.0.1-SNAPSHOT + jar + Resilient + Description for Resilient + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + + + + + + 3.2.5 + 17 + v20.14.0 + 10.8.1 + UTF-8 + UTF-8 + yyyyMMddHHmmss + ${java.version} + ${java.version} + com.oguerreiro.resilient.ResilientApp + -Djava.security.egd=file:/dev/./urandom -Xmx1G + jdt_apt + false + ${project.parent.version} + 1.3.0 + 10.17.0 + 1.11 + 1.15.0 + 8.0.2 + 0.8.12 + 8.5.0 + amd64 + eclipse-temurin:17-jre-focal + 3.4.2 + 1.0.0 + + + + 1.5.5.Final + 3.1.0 + 3.3.1 + 3.3.2 + 3.13.0 + 3.5.0 + 3.2.5 + 3.4.1 + 3.6.3 + 3.3.1 + 3.12.1 + 3.2.5 + 3.4.0 + 2.9.0 + 0.0.11 + + + + + + 1.2.1 + 3.11.0.3922 + 2.43.0 + 2.5.0 + + + + + tech.jhipster + jhipster-framework + ${jhipster-framework.version} + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + org.springframework.boot + spring-boot-loader-tools + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-test + test + + + org.springframework.security + spring-security-data + + + org.springframework.security + spring-security-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-api + ${springdoc-openapi-starter-webmvc-api.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate6 + + + com.fasterxml.jackson.datatype + jackson-datatype-hppc + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + + + com.mysql + mysql-connector-j + + + com.tngtech.archunit + archunit-junit5-api + ${archunit-junit5.version} + test + + + + + com.tngtech.archunit + archunit-junit5-engine + ${archunit-junit5.version} + test + + + com.zaxxer + HikariCP + + + io.micrometer + micrometer-registry-prometheus-simpleclient + + + jakarta.annotation + jakarta.annotation-api + + + javax.cache + cache-api + + + org.apache.commons + commons-lang3 + + + org.ehcache + ehcache + jakarta + + + org.glassfish.jaxb + jaxb-runtime + provided + + + org.hibernate.orm + hibernate-core + + + org.hibernate.orm + hibernate-jcache + + + org.hibernate.orm + hibernate-jpamodelgen + provided + + + org.hibernate.validator + hibernate-validator + + + org.liquibase + liquibase-core + ${liquibase.version} + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + org.testcontainers + jdbc + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mysql + test + + + org.testcontainers + testcontainers + test + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + + + com.diffplug.spotless + spotless-maven-plugin + + + com.google.cloud.tools + jib-maven-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.codehaus.mojo + properties-maven-plugin + + + org.gaul + modernizer-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + org.sonarsource.scanner.maven + sonar-maven-plugin + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + ${start-class} + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + + + + + + spotless + process-sources + + apply + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + target + ${node.version} + ${npm.version} + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${jib-maven-plugin.image} + + + ${jib-maven-plugin.architecture} + linux + + + + + resilient:latest + + + + bash + + /entrypoint.sh + + + 8081 + + + ALWAYS + 0 + + USE_CURRENT_TIMESTAMP + 1000 + + + src/main/docker/jib + + + /entrypoint.sh + 755 + + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + ${git-commit-id-maven-plugin.version} + + + + revision + + + + + false + false + true + + ^git.commit.id.abbrev$ + ^git.commit.id.describe$ + ^git.branch$ + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + ${checksum-maven-plugin.version} + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven-antrun-plugin.version} + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + io.spring.nohttp + nohttp-checkstyle + ${nohttp-checkstyle.version} + + + + checkstyle.xml + pom.xml,README.md + .git/**/*,target/**/*,node_modules/**/* + ./ + + + + + check + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + true + + + org.springframework.boot + spring-boot-configuration-processor + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.hibernate.orm + hibernate-jpamodelgen + + + org.glassfish.jaxb + jaxb-runtime + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-versions + + enforce + + + + enforce-dependencyConvergence + + + + + false + + + enforce + + + + + + + You are running an older version of Maven. JHipster requires at least Maven ${maven.version} + [${maven.version},) + + + You are running an incompatible version of Java. JHipster supports JDK 17 to 21. + [17,18),[18,19),[19,20),[20,21),[21,22) + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + + ${project.build.outputDirectory} + alphabetical + + **/*IT* + **/*IntTest* + + @{argLine} -Dspring.profiles.active=${profile.test} + + + + integration-test + + integration-test + + + + verify + + verify + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + ${maven.compiler.source} + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + config-resources + validate + + copy-resources + + + ${project.build.directory}/classes + + + src/main/resources/ + true + + config/*.yml + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + alphabetical + + **/*IT* + **/*IntTest* + + + src/test/resources/logback.xml + + + + + org.apache.maven.plugins + maven-war-plugin + ${maven-war-plugin.version} + + + default-war + + war + + package + + + + WEB-INF/**,META-INF/** + false + target/classes/static/ + + + src/main/webapp + + WEB-INF/** + + + + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-maven-plugin.version} + + + initialize + + read-project-properties + + + + sonar-project.properties + + + + + + + org.gaul + modernizer-maven-plugin + ${modernizer-maven-plugin.version} + + + modernizer + package + + modernizer + + + + + ${java.version} + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + pre-unit-tests + + prepare-agent + + + + + post-unit-test + test + + report + + + + pre-integration-tests + + prepare-agent-integration + + + + + post-integration-tests + post-integration-test + + report-integration + + + + + + org.liquibase + liquibase-maven-plugin + ${liquibase.version} + + config/liquibase/master.xml + ${project.basedir}/src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml + com.mysql.cj.jdbc.Driver + ${liquibase-plugin.url} + + ${liquibase-plugin.username} + ${liquibase-plugin.password} + hibernate:spring:com.oguerreiro.resilient.domain?dialect=org.hibernate.dialect.MySQL8Dialect&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + true + debug + !test + + + + org.liquibase + liquibase-core + ${liquibase.version} + + + org.liquibase.ext + liquibase-hibernate6 + ${liquibase.version} + + + jakarta.validation + jakarta.validation-api + ${jakarta-validation.version} + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring-boot.version} + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + + + + + + api-docs + + ,api-docs + + + + dev + + true + + + + dev${profile.tls}${profile.no-liquibase} + testdev + jdbc:mysql://localhost:3306/resilient + root + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + + eclipse + + + m2e.version + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + + org.eclipse.m2e + lifecycle-mapping + ${lifecycle-mapping.version} + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + prepare-agent + + + + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + install-node-and-npm + npm + + + + + + + + + + + + + + + + + IDE + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.hibernate.orm + hibernate-jpamodelgen + + + + + no-liquibase + + ,no-liquibase + + + + prod + + + prod${profile.api-docs}${profile.tls}${profile.e2e}${profile.no-liquibase} + testprod + jdbc:mysql://localhost:3306/resilient + root + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + target/classes/static/ + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build test + + npm + + test + + run webapp:test + false + + + + webapp build prod + + npm + + generate-resources + + run webapp:prod + + ${project.version} + + false + + + + + + + + + tls + + ,tls + + + + war + + + + org.apache.maven.plugins + maven-war-plugin + + + + + + webapp + + true + + + + dev${profile.no-liquibase} + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + + + create-pre-compiled-webapp-checksum + + files + + generate-resources + + + create-compiled-webapp-checksum + + files + + compile + + checksums.csv.old + + + + + + + ${project.basedir} + + src/main/webapp/**/*.* + target/classes/static/**/*.* + package-lock.json + package.json + tsconfig.json + tsconfig.app.json + webpack/*.* + + + **/app/**/service-worker.js + **/app/**/vendor.css + + + + false + false + false + + SHA-1 + + true + true + + + + org.apache.maven.plugins + maven-antrun-plugin + + + eval-frontend-checksum + generate-resources + + run + + + + + + + + + + + + true + + + + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build dev + + npm + + generate-resources + + run webapp:build + + ${project.version} + + false + + + + + + + + + diff --git a/resilient-mono.jh b/resilient-mono.jh new file mode 100644 index 0000000..1513e5a --- /dev/null +++ b/resilient-mono.jh @@ -0,0 +1,788 @@ + +/* + * Resilient JHipster JDL file definition. + * DON'T use infinite-scroll. The infinite-scroll paginate option + * has some kind of error, and makes it IMPOSSIBLE to login into the + * APP. + */ + +// POST generation metadata +// This metadata set the order of fields. And the only ones visible. All others, will be hidden +// @@ List:: < A list of properties for the list view. Ex: code;name;description > +// @@ Update:: < A list of properties for the Update/Create form. Ex: code;name;description > +// @@ View:: < A list of properties for the Detail form. Ex: code;name;description > +// The properties in this list, will be EDITABLE only when in CREATE mode. Readonly otherwise +// @@ READONLY:: < A list of properties for the Detail form. Ex: code;name;description > +// @@ IMAGE:: + +/** + * Monolith app + * + * Resilient core functions microservice. + */ +application { + config { + baseName resilient + packageName com.oguerreiro.resilient + applicationType monolith + authenticationType session + devDatabaseType mysql + prodDatabaseType mysql + clientFramework angularX + buildTool maven + cacheProvider ehcache + enableHibernateCache true + enableTranslation true + languages [pt-pt, en] + nativeLanguage pt-pt + serverPort 8081 + } + + entities * +} + +/** + * ************************************************** + * Core - Entities & Relations + * ************************************************** + */ + +/** + * Organization Nature enum + * This identifies the nature of the organization entry level. + */ +enum OrganizationNature { + /** + * Represents an organizational level (company; department; university; ...) + * Rules : An Organization always starts with and entry level ORGANIZATION. + * Parent's allowed: none; ORGANIZATION; LEVEL + */ + ORGANIZATION, + + /** + * Represents a human. With or without a login to the APP. + * Rules : Must have a parent Organization entry + * Parent's allowed: any + */ + PERSON, + + /** + * Represents physical building. + * Rules : Must have a parent Organization entry + * Parent's allowed: ORGANIZATION; LEVEL + */ + FACILITY, + + /** + * Generic. Its a organizer, creating a level without any system meaning + * Rules : Must have a parent Organization entry + * Parent's allowed: any + */ + LEVEL +} + +/** + * Organization Type + * The organization type is a setup that defines the usage of the Organization level and other configurations. + */ +@dto(mapstruct) +entity OrganizationType { + // @@ List:: icon;name;description + // @@ Update:: code;name;description;nature;icon;metadataProperties + // @@ View:: code;name;description;nature;icon;metadataProperties + // @@ READONLY:: code;nature + // @@ IMAGE:: icon + + /** User defined code. Must be unique in the OrganizationType */ + code String required minlength(3) unique + /** The name of the type. Must be unique in the system */ + name String required minlength(3) unique + /** A more complete description of the type. Whats the meaning of this type? */ + description String required minlength(3) + /** The nature of the type */ + nature OrganizationNature required + /** Icon image associated with this organization type. Will be used in user interface. */ + icon ImageBlob + + /** + * Record version for concurrency control. + */ + version Integer +} + +/** + * Organization + * Hierarchical organigram of the organization. + */ +@dto(mapstruct) +entity Organization { + // @@ List:: organizationType;code;name;parent + // @@ Update:: parent;organizationType;code;name;image;inputInventory;outputInventory;user + // @@ View:: parent;organizationType;code;name;image;inputInventory;outputInventory;user + // @@ READONLY:: parent;organizationType;code + + //References: + /* + * Applies only to OrganizationType.OrganizationNature.PERSON. + * Associates a PERSON with a login User. It's NOT mandatory that all PERSON have a system user + */ + //to User(user) + //to Organization(parent) + //to OrganizationType(organizationType) + + /** User defined code. Must be unique in the Organization */ + code String required minlength(3) unique + /** The name of the Organization entry. Allows duplicate name's, but NOT duplicate code's. */ + name String required minlength(3) + /* Optional image associated with this hierarchical level. NOT the same as OrganizationType.icon. + * The Organization.imagem is not an icon and its intended to be specific of the entry it self + */ + image ImageBlob + /* + * Applies only to OrganizationType.OrganizationNature.ORGANIZATION. + * Means that this level is allowed to have inventory data input. Data can be inserted into table InputData associated to this Organization + */ + inputInventory Boolean + + /* + * Applies only to OrganizationType.OrganizationNature.ORGANIZATION. + * Means that this level will be evaluated for inventory output variables. + */ + outputInventory Boolean + + /** + * Record version for concurrency control. + */ + version Integer +} + +// Metadata is a structure for any info associated with other entity domains. +// It's general information that can be used, for example, in formulas. + +/** + * Metadata type enum + * The property type + */ +enum MetadataType { + STRING, + BOOLEAN, + INTEGER, + DECIMAL, + DATE +} + +/** + * Metadata Property + * Configuration of a metadata property. Defines its type and a description of its usage. + */ +@dto(mapstruct) +entity MetadataProperty { + // @@ List:: code;name;description;mandatory + // @@ Update:: code;name;description;metadataTye;mandatory;pattern + // @@ View:: code;name;description;metadataTye;mandatory;pattern + // @@ READONLY:: code;metadataType + + code String required minlength(3) unique + name String required + description String + mandatory Boolean + metadataType MetadataType required + /** Regex pattern for validation (optional). Only applies when the value is not empty. */ + pattern String + + /** + * Record version for concurrency control. + */ + version Integer +} + +/** + * Metadata Value + * The value of a metadata property. + */ +@dto(mapstruct) +entity MetadataValue { + /** Functional relational key to MetadataProperty */ + metadataPropertyCode String required + + /** + * The target domain key that this MetadataValue belongs. Its a class name or a key provided by the consumer.
+ * Used with targetDomainId to create as unique relation with the target domain + * @see targetDomainId + */ + targetDomainKey String required + + /** + * The target domain Id that this MetadataValue belongs.
+ * Used with targetDomainKey to create as unique relation with the target domain + * @see targetDomainKey + */ + targetDomainId Long required + + /** + * The value of the MetadataProperty, persisted as a string.
+ */ + value String + + /** + * Record version for concurrency control. + */ + version Integer +} + +relationship OneToMany { + Organization{child(name)} to Organization{parent(name)} + OrganizationType to Organization{organizationType(name)} +} + +relationship OneToOne { + /* Relation to builtInEntity User, must have the name "user", any other, JHipster generates TC with error */ + Organization{user(login)} to User with builtInEntity +} + +relationship ManyToMany { + // NOTE: For this to work, the OrganizationTypeMapper.toDtoMetadataPropertyName(MetadataProperty metadataProperty) + // Must have an additional mapping for the version property : @Mapping(target = "version", source = "version") + OrganizationType{metadataProperties(name)} to MetadataProperty{organizationType(name)} +} + +/** + * ************************************************** + * Inventory - Entities & Relations + * ************************************************** + */ + + /** + * Variable InputMode enum + * Defines how the input values for the variable are inserted. + */ +enum InputMode { + /** Values are inserted by DataFile only. User can always change the value manually, but they can't delete it or insert it */ + DATAFILE, + /** Values are inserted Manually. In this mode, only a human can insert and change the variable values. If a file or integration attempts to insert it, an error will be thrown */ + MANUAL, + /** Values are inserted by Integration files. Very similar to the option DATAFILE */ + INTEGRATION, + /** The variable can be inserted by any of the modes */ + ANY +} + + /** + * Unit Variable Type + * The type of data this unit represents + */ +enum UnitValueType { + /** Decimal value */ + DECIMAL, + /** boolean value */ + BOOLEAN +} + +/** + * UnitType + * Type of measure unit types. Convertions can only be done within Unit's of the same UnitType + */ +@dto(mapstruct) +entity UnitType { + // @@ List:: name;description;valueType + // @@ Update:: name;description;valueType + // @@ View:: name;description;valueType + + name String required unique + description String required + valueType UnitValueType required + + /** + * Record version for concurrency control. + */ + version Integer +} + +/** + * Unit + */ +@dto(mapstruct) +entity Unit { + // @@ List:: unitType;symbol;name;description + // @@ Update:: unitType;code;symbol;name;description;convertionRate + // @@ View:: unitType;code;symbol;name;description;convertionRate + // @@ Readonly:: unitType;code + + /** Required code to use in integrations */ + code String required unique + /** Math symbol that is usually used as sufix for this unit. Eg. 'm3' for square metters.*/ + symbol String required + /** Math name for this unit. Eg. 'Square metters'.*/ + name String required + description String + + /** + * The convertion rate (CR), relative to the base unit. + * This values awnsers the question: When the base unit is 1 (one), whats the value of this unit measure ? + * Ex: How many Ml (miligrams) 1Kg has? 1000 + * The formula to convert between any value unit the scale is: + * *CR(target unit)/CR(source unit) + */ + convertionRate BigDecimal required + + /** + * Record version for concurrency control. + */ + version Integer +} + +/** + * Unit Converter + */ +@dto(mapstruct) +entity UnitConverter { + // @@ List:: fromUnit;toUnit;name;description + // @@ Update:: fromUnit;toUnit;name;description;convertionFormula + // @@ View:: fromUnit;toUnit;name;description;convertionFormula + // @@ Readonly:: fromUnit;toUnit + + //References: + //to Unit(fromUnit) + //to Unit(toUnit) + + name String required + description String required + + /** + * The convertion formula to convert the fromUnit into the toUnit. + */ + convertionFormula String required + + /** + * Record version for concurrency control. + */ + version Integer +} + +// Standard GEE Variable definition schema +// VariableScope +// (1-n) VariableCategory +// (1-n) Variable +// --------------------------------------------------- + +/** + * Variable Scope + * Example: GEE Scope 1/2/3 or General Info + */ +@dto(mapstruct) +entity VariableScope { + // @@ List:: code;name;description + // @@ Update:: code;name;description + // @@ View:: code;name;description + // @@ Readonly:: code + + /** + * Unique key, user defined. + * Be aware, that this will be used to create the Variable.code + * NOTE: The field type is string, but restricted to only numbers + */ + code String required pattern(/^[0-9]*$/) unique + /** The name or title for this scope */ + name String required + /** A description or usage of this scope */ + description String required + + /** Record version for concurrency control. */ + version Integer +} + +/** + * Variable Category + * Sub-division of VariableScope in Categories + */ +@dto(mapstruct) +entity VariableCategory { + // @@ List:: code;name;description + // @@ Update:: variableScope;code;name;description + // @@ View:: variableScope;code;name;description + // @@ Readonly:: variableScope;code + + //References: + //to VariableScope + + /** + * Unique key, user defined. + * Be aware, that this will be used to create the Variable.code + * NOTE: The field type is string, but restricted to only numbers + */ + code String required pattern(/^[0-9]*$/) + /** The name or title for this category */ + name String required + /** A description or usage of this category */ + description String required + + /** Record version for concurrency control. */ + version Integer +} + +/** + * Variable Definition (GEE variable classification) + */ +@dto(mapstruct) +entity Variable { + // @@ List:: name;description;input;output + // @@ Update:: variableScope;variableCategory;code;name;baseUnit;description + // @@ View:: variableScope;variableCategory;code;name;baseUnit;description + // @@ Readonly:: variableScope;variableCategory;code + + //References: + //to VariableScope + //to VariableCategory + //to Unit (baseUnit) - The unit of the variable + + /** + * Unique key, composed by:
+ * VariableScope.code + VariableCategory.code + + * NOTA: User will provide the index property, and the software will create the code + * index and code are NOT changeable + */ + code String required pattern(/^[0-9]*$/) unique + /** Variable index */ + variableIndex Integer required + /** The name or title for the variable */ + name String required + /** The descriptions of the variable. Its meaning. Its usage. */ + description String required + /** Defines this variable as input. Values will be inserted for this variable */ + input Boolean required + /** Defines the way of input. See the enum for instructions. */ + inputMode InputMode required + /** Defines this variable as output. This variable will be evaluated for output */ + output Boolean required + /** The formula to calculate the output valut of the variable. Mandatory when output==TRUE */ + outputFormula String + + /** Record version for concurrency control. */ + version Integer +} +filter Variable + +/** + * Variable allowed input units. Also defines the converters needed if the + */ +@dto(mapstruct) +entity VariableUnits { + // @@ List:: variable;unit + // @@ Update:: variable;unit + // @@ View:: variable;unit + // @@ Readonly:: variable;unit + + //References: + //to Variable + //to Unit (unit) - The allowed unit for input + + /** Record version for concurrency control. */ + version Integer +} + +/** + * Variable Class + * A sub-variable classification. Example: Variable=FUEL CONSUMPTIOM; CLASS=GASOLINE; OR ELECTRICAL; ETC... + */ +@dto(mapstruct) +entity VariableClass { + // @@ List:: variable;code;name + // @@ Update:: variable;code;name + // @@ View:: variable;code;name + // @@ Readonly:: variable;code + + //References: + //to Variable + + /** Code of the class. Must be unique within the Variavle */ + code String required + /** Human name of the class. Must be unique within the Variable, because it will be used as key for the Excel integration data. */ + name String required + + /** Record version for concurrency control. */ + version Integer +} + +// Period schema +// Period (1-n) PeriodVersion +// --------------------------------------------------- + +/** + * Period status enum + */ +enum PeriodStatus { + OPEN, + REOPEN, + CLOSED +} + +/** + * Data Source enum + */ +enum DataSourceType { + MANUAL, + FILE, + AUTO, + /** Special case, where a InputData value from an Organization is distributed to other Organization's */ + DISTRIB +} + +/** + * Upload file type enum + */ +enum UploadType { + /** Custom Excel file template, for variable input upload */ + INVENTORY, + + /** Exported file from the accounting software */ + ACCOUNTING +} + +/** + * Upload file status enum + */ +enum UploadStatus { + UPLOADED, + PROCESSING, + PROCESSED, + ERROR +} + +/** + * Period + */ +@dto(mapstruct) +entity Period (period) { + // @@ List:: name;description;beginDate;endDate;state + // @@ Update:: name;description;beginDate;endDate;state + // @@ View:: name;description;beginDate;endDate;state + + //References: + //? + + /** Name of the period */ + name String required + /** Description for the period */ + description String + /** Period start date */ + beginDate LocalDate required + /** Period end date */ + endDate LocalDate required + /** State of the Period */ + state PeriodStatus required + + /** The creation date (insert). When the data was inserted into the system. */ + creationDate Instant required + /** The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username. */ + creationUsername String required + /** Record version for concurrency control. */ + version Integer +} + +/** + * Period versions + * Always have at least one version. + */ +@dto(mapstruct) +entity PeriodVersion { + //References: + //to Period + + /** Name of the version */ + name String required + /** Description for the version */ + description String + /** Period version (sequencial) */ + periodVersion Integer required + /** State of the Period */ + state PeriodStatus required + + /** The creation date (insert). When the data was inserted into the system. */ + creationDate Instant required + /** The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username. */ + creationUsername String required + /** Record version for concurrency control. */ + version Integer +} + +/** + * Input data Values + * The values inserted from: + * - Each Organic Unit + * - Variable + * - PeriodVersion + */ +@dto(mapstruct) +entity InputData { + //References: + //to Variable - The Variable that this data reports + //to Period - The period that this data belongs + //to PeriodVersion - The period version active, when the data was inserted + //to Unit (sourceUnit) - The Unit that the data was inserted. Must be one of the allowed input units for the variable + //to Unit (unit) - The Unit of the variableValue. Its the unit configured in Variable.baseUnit + //to Organization (owner) - The owner of the data. The Organization responsable for generating this data (that consumed this value) + //to InputData (inputData) - In case of InputData.source==DataSourceType.DISTRIB, this points to the parent owner InputData + //to InputDataUpload - In case of InputData.source==DataSourceType.FILE, this points to the InputDataUpload file that contains this data + + /** The value of the data in the source Unit. This might be different from the Variable baseUnit. @see variableValue and variableUnit */ + sourceValue BigDecimal required + /** The value of the data converted to the variable baseUnit. */ + variableValue BigDecimal required + /** + * The value that the InputData.owner is accounted for. + * In case a variableValue is distributed to other Organization's (InputData.source==DataSources.DISTRIB) + * , this will have the value to consider when calculating the final inventory value for the variable + */ + imputedValue BigDecimal required + /** What was the source of this data ? */ + sourceType DataSourceType required + /** + * The date that this data relates to. + * Eg. energy consumptium happens every month, this should be the date of invoice + * [optional] + */ + dataDate LocalDate + + /** Last change date */ + changeDate Instant required + /** The actual username logged into the system, that last changed the data */ + changeUsername String required + /** Where this data originates from? Where were the data collected ? */ + dataSource String + /** Who collected the data ? */ + dataUser String + /** Comments added by the user */ + dataComments String + + /** The creation date (insert). When the data was inserted into the system. */ + creationDate Instant required + /** The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username. */ + creationUsername String required + /** Record version for concurrency control. */ + version Integer +} + +/** + * Input data upload file + * Users can upload an Excel file with InputData that will automatically insert values in the table InputData + */ +@dto(mapstruct) +entity InputDataUpload { + // @@ List:: title;type;state + // @@ Update:: title;type;dataFile;comments;state + // @@ View:: title;type;dataFile;comments:state + + //References: + //to Period - The period that this data belongs + //to PeriodVersion - The period version active, when the data was inserted + //to Organization (owner) - The owner of the data. The Organization responsable for generating this data (that consumed this value) + + /** A simple text with a title, for human reference and better relation */ + title String required + /** An Excel file, that must match a template for input */ + dataFile Blob required + /** The name of the uploaded Excel file */ + uploadFileName String + /** The generated name of the Excel file, used to save to disk */ + diskFileName String + /** Type of the Upload */ + type UploadType required + /** State of the Upload */ + state UploadStatus required + /** Optional comments */ + comments String + + /** The creation date (insert). When the data was inserted into the system. */ + creationDate Instant required + /** The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username. */ + creationUsername String required + /** Record version for concurrency control. */ + version Integer +} + +/** + * Input data upload file + * Users can upload an Excel file with InputData that will automatically insert values in the table InputData + */ +@dto(mapstruct) +entity InputDataUploadLog { + //References: + //to InputDataUpload - The InputDataUpload that this log belongs + + /** The log of the event. Eg. "InputDataUpload created"; "InputDataUpload replaced with new file"; "125 records inserted"; "User XPTO confirmed the file replacement." */ + logMessage String required + + /** The creation date (insert). When the data was inserted into the system. */ + creationDate Instant required + /** The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username. */ + creationUsername String required + /** Record version for concurrency control. */ + version Integer +} + +/** + * Output data Values + * The values calculated using the Variable.outputFormula, for all variables with Variable.output==TRUE. + */ +@dto(mapstruct) +entity OutputData { + //References: + //to Variable - The Variable that this data reports + //to Period - The period that this data belongs + //to PeriodVersion - The period version active, when the data was inserted + //to Unit (baseUnit) - The Unit of the variableValue. Its the unit configured in Variable.baseUnit + //to Organization (owner) - The owner of the data. The Organization responsable for generating this data (that consumed this value) + + /** The evaluated value, calculated by Variable.outputFormula. This will probably represent an emission value. */ + value BigDecimal required + + /** Record version for concurrency control. */ + version Integer +} + +relationship OneToMany { + UnitType to Unit{unitType(name)} + Unit{fromUnit(name)} to UnitConverter{fromUnit(name)} + Unit{toUnit(name)} to UnitConverter{toUnit(name)} + Unit{baseUnit(name)} to Variable{baseUnit(name)} + + VariableScope to VariableCategory{variableScope(name)} + VariableScope to Variable {variableScope(name)} + VariableCategory to Variable {variableCategory(name)} + Variable to VariableClass{variable(name)} + Period to PeriodVersion{period(name)} + + //InputData references + Variable to InputData{variable(name)} + Period to InputData{period(name)} + PeriodVersion to InputData{periodVersion(name)} + Unit{sourceUnit} to InputData{sourceUnit(name)} + Unit to InputData{unit(name)} + Organization to InputData{owner(name)} + InputData to InputData{sourceInputData} + InputDataUpload to InputData{sourceInputDataUpload} + + //InputDataUpload + Period to InputDataUpload{period(name)} + PeriodVersion to InputDataUpload{periodVersion(name)} + Organization to InputDataUpload{owner(name)} + + //OutputData references + Variable to OutputData{variable(name)} + Period to OutputData{period(name)} + PeriodVersion to OutputData{periodVersion(name)} + Unit to OutputData{baseUnit(name)} + Organization to InputData{owner(name)} + + //VariableUnits + Variable to VariableUnits{variable(name)} + Unit to VariableUnits{unit(name)} + + //InputDataUploadLog + InputDataUpload to InputDataUploadLog{inputDataUpload(title)} +} + +/** paginate Entry, Tag with infinite-scroll */ +// paginate Organization, Metadata with pagination + +service * with serviceClass diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..4081287 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,40 @@ +sonar.projectKey = resilient +sonar.projectName = resilient generated by jhipster + +# Typescript tests files must be inside sources and tests, otherwise `INFO: Test execution data ignored for 80 unknown files, including:` +# is shown. +sonar.sources = src +sonar.tests = src +sonar.host.url = http://localhost:9001 + +sonar.test.inclusions = src/test/**/*.*, src/main/webapp/app/**/*.spec.ts +sonar.coverage.jacoco.xmlReportPaths = target/site/**/jacoco*.xml +sonar.java.codeCoveragePlugin = jacoco +sonar.junit.reportPaths = target/surefire-reports,target/failsafe-reports +sonar.testExecutionReportPaths = target/test-results/jest/TESTS-results-sonar.xml +sonar.javascript.lcov.reportPaths = target/test-results/lcov.info + +sonar.sourceEncoding = UTF-8 +sonar.exclusions = src/main/webapp/content/**/*.*, src/main/webapp/i18n/*.js, target/classes/static/**/*.* + +sonar.issue.ignore.multicriteria = S1192,S125,S3437,S4684,S5145,UndocumentedApi + +# Rule https://rules.sonarsource.com/java/RSPEC-3437 is ignored, as a JPA-managed field cannot be transient +sonar.issue.ignore.multicriteria.S3437.resourceKey = src/main/java/**/* +sonar.issue.ignore.multicriteria.S3437.ruleKey = squid:S3437 +# Rule https://rules.sonarsource.com/java/RSPEC-4684 +sonar.issue.ignore.multicriteria.S4684.resourceKey = src/main/java/**/* +sonar.issue.ignore.multicriteria.S4684.ruleKey = java:S4684 +# Rule https://rules.sonarsource.com/java/RSPEC-5145 log filter is applied +sonar.issue.ignore.multicriteria.S5145.resourceKey = src/main/java/**/* +sonar.issue.ignore.multicriteria.S5145.ruleKey = javasecurity:S5145 +# Rule https://rules.sonarsource.com/java/RSPEC-1176 is ignored, as we want to follow "clean code" guidelines and classes, methods and +# arguments names should be self-explanatory +sonar.issue.ignore.multicriteria.UndocumentedApi.resourceKey = src/main/java/**/* +sonar.issue.ignore.multicriteria.UndocumentedApi.ruleKey = squid:UndocumentedApi +# Rule https://rules.sonarsource.com/java/RSPEC-1192 +sonar.issue.ignore.multicriteria.S1192.resourceKey = src/main/java/**/CacheConfiguration.java +sonar.issue.ignore.multicriteria.S1192.ruleKey = java:S1192 +# Rule https://rules.sonarsource.com/xml/RSPEC-125 +sonar.issue.ignore.multicriteria.S125.resourceKey = src/main/resources/logback-spring.xml +sonar.issue.ignore.multicriteria.S125.ruleKey = xml:S125 diff --git a/src/main/docker-mariadb/Readme b/src/main/docker-mariadb/Readme new file mode 100644 index 0000000..30f9b02 --- /dev/null +++ b/src/main/docker-mariadb/Readme @@ -0,0 +1,77 @@ +# ###################################################### # +# MariaDB # +# # +# Database container with MariaDB # +# ###################################################### # + +Project Structure: +================== + +docker-nginx/ +├── docker-compose.yml +├── mariadb_data/ +│ ├── # Volume for database files + + +Build & Run: +============ + +For a first startup: + + > docker-compose up --build + +For a startup: + + > docker-compose up -d + + +Users: +====== +User: resDbUser +Pwd : res#Db!Pwd%765 + + +Config: +======= +User Root Password: root#unl!853 +Port: 3406 + + +Create database +=============== + + #2 Create database + > docker exec -it mariadb mariadb -uroot -proot#unl!853 -e "CREATE DATABASE IF NOT EXISTS resilient_resilient;" + , its the docker container name + , THE SECOND is the equivalent to mysql command, but for mariadb + , the root password + , its the intended database name + + #2 Verify + > docker exec -it mariadb mariadb -uroot -proot#unl!853 -e "SHOW DATABASES;" + , its the docker container name + , THE SECOND is the equivalent to mysql command, but for mariadb + , the root password + +Restore from mysqldump +====================== + + #1 Copy dump file into the ./dumps directory + + #2 Import the dump into the database, without copy the dump into container + > docker exec -i mariadb mariadb -uroot -proot#unl!853 resilient_resilient < ./dumps/resilient_dump.sql + , its the docker container name + , THE SECOND is the equivalent to mysql command, but for mariadb + , the root password + , its the intended database name + , the dump file name + + #3 Verify (SHOW TABLES;) + > docker exec -it mariadb mariadb -uresDbUser -pres#Db!Pwd%765 resilient_resilient -e "SHOW TABLES;" + , its the docker container name + , THE SECOND is the equivalent to mysql command, but for mariadb + , User to use. NOTE: Testing the functional user (not root). + , The user password. NOTE: Testing the functional user (not root). + , its the intended database name + + \ No newline at end of file diff --git a/src/main/docker-mariadb/config/my.cnf b/src/main/docker-mariadb/config/my.cnf new file mode 100644 index 0000000..489ef7e --- /dev/null +++ b/src/main/docker-mariadb/config/my.cnf @@ -0,0 +1,2 @@ +[mysqld] +port = 3306 diff --git a/src/main/docker-mariadb/docker-compose.yml b/src/main/docker-mariadb/docker-compose.yml new file mode 100644 index 0000000..3cd239a --- /dev/null +++ b/src/main/docker-mariadb/docker-compose.yml @@ -0,0 +1,18 @@ +services: + mariadb: + image: mariadb:11.7.2 + container_name: mariadb + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root#unl!853 + MYSQL_DATABASE: resilient_resilient + MYSQL_USER: resDbUser + MYSQL_PASSWORD: res#Db!Pwd%765 + ports: + - "3306:3306" + volumes: + - ./mariadb_data:/var/lib/mysql + - ./config/my.cnf:/etc/mysql/conf.d/custom.cnf:ro # ':ro' means 'readonly', this is mandatory for MariaDB, or it will IGNORE the file + +volumes: + mariadb_data: diff --git a/src/main/docker-mariadb/dumps/.keep b/src/main/docker-mariadb/dumps/.keep new file mode 100644 index 0000000..b9ac995 --- /dev/null +++ b/src/main/docker-mariadb/dumps/.keep @@ -0,0 +1,2 @@ +File to force directory commit into SVN. +The content is SVN:IGNORED \ No newline at end of file diff --git a/src/main/docker-mariadb/mariadb_data/.keep b/src/main/docker-mariadb/mariadb_data/.keep new file mode 100644 index 0000000..b9ac995 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/.keep @@ -0,0 +1,2 @@ +File to force directory commit into SVN. +The content is SVN:IGNORED \ No newline at end of file diff --git a/src/main/docker-mariadb/mariadb_data/.my-healthcheck.cnf b/src/main/docker-mariadb/mariadb_data/.my-healthcheck.cnf new file mode 100644 index 0000000..73e00b1 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/.my-healthcheck.cnf @@ -0,0 +1,6 @@ +[mariadb-client] +port=3306 +socket=/run/mysqld/mysqld.sock +user=healthcheck +password={lW`(KQ*F1l 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=642fe6a7c3b6108d6b59443fc10c462f +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365778934 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n event_name,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(max_timer_wait) AS max_latency\n FROM performance_schema.events_waits_summary_by_host_by_event_name\n WHERE event_name LIKE \'wait/io/file%\'\n AND count_star > 0\n ORDER BY IF(host IS NULL, \'background\', host), sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` like \'wait/io/file%\' and `performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` > 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_stages.frm b/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_stages.frm new file mode 100644 index 0000000..7599a2f --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_stages.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency` from `performance_schema`.`events_stages_summary_by_host_by_event_name` where `performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=338c41d6e3c505c9bbff217479a811f9 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365858446 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n event_name,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(avg_timer_wait) AS avg_latency\n FROM performance_schema.events_stages_summary_by_host_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY IF(host IS NULL, \'background\', host), sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency` from `performance_schema`.`events_stages_summary_by_host_by_event_name` where `performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_statement_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_statement_latency.frm new file mode 100644 index 0000000..9bb0187 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_statement_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(max(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME`)) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=41a7ff7a1fc9ad52daba6e441887ef43 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365838824 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n SUM(count_star) AS total,\n format_pico_time(SUM(sum_timer_wait)) AS total_latency,\n format_pico_time(MAX(max_timer_wait)) AS max_latency,\n format_pico_time(SUM(sum_lock_time)) AS lock_latency,\n SUM(sum_rows_sent) AS rows_sent,\n SUM(sum_rows_examined) AS rows_examined,\n SUM(sum_rows_affected) AS rows_affected,\n SUM(sum_no_index_used) + SUM(sum_no_good_index_used) AS full_scans\n FROM performance_schema.events_statements_summary_by_host_by_event_name\n GROUP BY IF(host IS NULL, \'background\', host)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(max(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME`)) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_statement_type.frm b/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_statement_type.frm new file mode 100644 index 0000000..288c253 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/host_summary_by_statement_type.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,substring_index(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` where `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=1be13212fa0d3d40dd76ca1dcf43f555 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365819242 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n SUBSTRING_INDEX(event_name, \'/\', -1) AS statement,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(max_timer_wait) AS max_latency,\n format_pico_time(sum_lock_time) AS lock_latency,\n sum_rows_sent AS rows_sent,\n sum_rows_examined AS rows_examined,\n sum_rows_affected AS rows_affected,\n sum_no_index_used + sum_no_good_index_used AS full_scans\n FROM performance_schema.events_statements_summary_by_host_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY IF(host IS NULL, \'background\', host), sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,substring_index(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` where `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/innodb_buffer_stats_by_schema.frm b/src/main/docker-mariadb/mariadb_data/sys/innodb_buffer_stats_by_schema.frm new file mode 100644 index 0000000..7643eb0 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/innodb_buffer_stats_by_schema.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,`sys`.`format_bytes`(sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`))) AS `allocated`,`sys`.`format_bytes`(sum(`ibp`.`DATA_SIZE`)) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED` = \'YES\',1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD` = \'YES\',1,NULL)) AS `pages_old`,round(sum(`ibp`.`NUMBER_RECORDS`) / count(distinct `ibp`.`INDEX_NAME`),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +md5=178653a8e67a4b42359c7a859d5b8410 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298364977621 +create-version=2 +source=SELECT IF(LOCATE(\'.\', ibp.table_name) = 0, \'InnoDB System\', REPLACE(SUBSTRING_INDEX(ibp.table_name, \'.\', 1), \'`\', \'\')) AS object_schema,\n sys.format_bytes(SUM(IF(ibp.compressed_size = 0, 16384, compressed_size))) AS allocated,\n sys.format_bytes(SUM(ibp.data_size)) AS data,\n COUNT(ibp.page_number) AS pages,\n COUNT(IF(ibp.is_hashed = \'YES\', 1, NULL)) AS pages_hashed,\n COUNT(IF(ibp.is_old = \'YES\', 1, NULL)) AS pages_old,\n ROUND(SUM(ibp.number_records)/COUNT(DISTINCT ibp.index_name)) AS rows_cached\n FROM information_schema.innodb_buffer_page ibp\n WHERE table_name IS NOT NULL\n GROUP BY object_schema\n ORDER BY SUM(IF(ibp.compressed_size = 0, 16384, compressed_size)) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,`sys`.`format_bytes`(sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`))) AS `allocated`,`sys`.`format_bytes`(sum(`ibp`.`DATA_SIZE`)) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED` = \'YES\',1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD` = \'YES\',1,NULL)) AS `pages_old`,round(sum(`ibp`.`NUMBER_RECORDS`) / count(distinct `ibp`.`INDEX_NAME`),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/innodb_buffer_stats_by_table.frm b/src/main/docker-mariadb/mariadb_data/sys/innodb_buffer_stats_by_table.frm new file mode 100644 index 0000000..21f9899 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/innodb_buffer_stats_by_table.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') AS `object_name`,`sys`.`format_bytes`(sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`))) AS `allocated`,`sys`.`format_bytes`(sum(`ibp`.`DATA_SIZE`)) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED` = \'YES\',1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD` = \'YES\',1,NULL)) AS `pages_old`,round(sum(`ibp`.`NUMBER_RECORDS`) / count(distinct `ibp`.`INDEX_NAME`),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')),replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +md5=5ddd96cfad4a231391cb2ea69bbaea79 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365005639 +create-version=2 +source=SELECT IF(LOCATE(\'.\', ibp.table_name) = 0, \'InnoDB System\', REPLACE(SUBSTRING_INDEX(ibp.table_name, \'.\', 1), \'`\', \'\')) AS object_schema,\n REPLACE(SUBSTRING_INDEX(ibp.table_name, \'.\', -1), \'`\', \'\') AS object_name,\n sys.format_bytes(SUM(IF(ibp.compressed_size = 0, 16384, compressed_size))) AS allocated,\n sys.format_bytes(SUM(ibp.data_size)) AS data,\n COUNT(ibp.page_number) AS pages,\n COUNT(IF(ibp.is_hashed = \'YES\', 1, NULL)) AS pages_hashed,\n COUNT(IF(ibp.is_old = \'YES\', 1, NULL)) AS pages_old,\n ROUND(SUM(ibp.number_records)/COUNT(DISTINCT ibp.index_name)) AS rows_cached\n FROM information_schema.innodb_buffer_page ibp\n WHERE table_name IS NOT NULL\n GROUP BY object_schema, object_name\n ORDER BY SUM(IF(ibp.compressed_size = 0, 16384, compressed_size)) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') AS `object_name`,`sys`.`format_bytes`(sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`))) AS `allocated`,`sys`.`format_bytes`(sum(`ibp`.`DATA_SIZE`)) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED` = \'YES\',1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD` = \'YES\',1,NULL)) AS `pages_old`,round(sum(`ibp`.`NUMBER_RECORDS`) / count(distinct `ibp`.`INDEX_NAME`),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')),replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/innodb_lock_waits.frm b/src/main/docker-mariadb/mariadb_data/sys/innodb_lock_waits.frm new file mode 100644 index 0000000..1f12608 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/innodb_lock_waits.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `r`.`trx_wait_started` AS `wait_started`,timediff(current_timestamp(),`r`.`trx_wait_started`) AS `wait_age`,timestampdiff(SECOND,`r`.`trx_wait_started`,current_timestamp()) AS `wait_age_secs`,`rl`.`lock_table` AS `locked_table`,`rl`.`lock_index` AS `locked_index`,`rl`.`lock_type` AS `locked_type`,`r`.`trx_id` AS `waiting_trx_id`,`r`.`trx_started` AS `waiting_trx_started`,timediff(current_timestamp(),`r`.`trx_started`) AS `waiting_trx_age`,`r`.`trx_rows_locked` AS `waiting_trx_rows_locked`,`r`.`trx_rows_modified` AS `waiting_trx_rows_modified`,`r`.`trx_mysql_thread_id` AS `waiting_pid`,`sys`.`format_statement`(`r`.`trx_query`) AS `waiting_query`,`rl`.`lock_id` AS `waiting_lock_id`,`rl`.`lock_mode` AS `waiting_lock_mode`,`b`.`trx_id` AS `blocking_trx_id`,`b`.`trx_mysql_thread_id` AS `blocking_pid`,`sys`.`format_statement`(`b`.`trx_query`) AS `blocking_query`,`bl`.`lock_id` AS `blocking_lock_id`,`bl`.`lock_mode` AS `blocking_lock_mode`,`b`.`trx_started` AS `blocking_trx_started`,timediff(current_timestamp(),`b`.`trx_started`) AS `blocking_trx_age`,`b`.`trx_rows_locked` AS `blocking_trx_rows_locked`,`b`.`trx_rows_modified` AS `blocking_trx_rows_modified`,concat(\'KILL QUERY \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_query`,concat(\'KILL \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_connection` from ((((`information_schema`.`innodb_lock_waits` `w` join `information_schema`.`innodb_trx` `b` on(`b`.`trx_id` = `w`.`blocking_trx_id`)) join `information_schema`.`innodb_trx` `r` on(`r`.`trx_id` = `w`.`requesting_trx_id`)) join `information_schema`.`innodb_locks` `bl` on(`bl`.`lock_id` = `w`.`blocking_lock_id`)) join `information_schema`.`innodb_locks` `rl` on(`rl`.`lock_id` = `w`.`requested_lock_id`)) order by `r`.`trx_wait_started` +md5=0fb2774411abbf295a443273d6c9e7c5 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365036655 +create-version=2 +source=SELECT r.trx_wait_started AS wait_started,\n TIMEDIFF(NOW(), r.trx_wait_started) AS wait_age,\n TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) AS wait_age_secs,\n rl.lock_table AS locked_table,\n rl.lock_index AS locked_index,\n rl.lock_type AS locked_type,\n r.trx_id AS waiting_trx_id,\n r.trx_started as waiting_trx_started,\n TIMEDIFF(NOW(), r.trx_started) AS waiting_trx_age,\n r.trx_rows_locked AS waiting_trx_rows_locked,\n r.trx_rows_modified AS waiting_trx_rows_modified,\n r.trx_mysql_thread_id AS waiting_pid,\n sys.format_statement(r.trx_query) AS waiting_query,\n rl.lock_id AS waiting_lock_id,\n rl.lock_mode AS waiting_lock_mode,\n b.trx_id AS blocking_trx_id,\n b.trx_mysql_thread_id AS blocking_pid,\n sys.format_statement(b.trx_query) AS blocking_query,\n bl.lock_id AS blocking_lock_id,\n bl.lock_mode AS blocking_lock_mode,\n b.trx_started AS blocking_trx_started,\n TIMEDIFF(NOW(), b.trx_started) AS blocking_trx_age,\n b.trx_rows_locked AS blocking_trx_rows_locked,\n b.trx_rows_modified AS blocking_trx_rows_modified,\n CONCAT(\'KILL QUERY \', b.trx_mysql_thread_id) AS sql_kill_blocking_query,\n CONCAT(\'KILL \', b.trx_mysql_thread_id) AS sql_kill_blocking_connection\n FROM information_schema.innodb_lock_waits w\n INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id\n INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id\n INNER JOIN information_schema.innodb_locks bl ON bl.lock_id = w.blocking_lock_id\n INNER JOIN information_schema.innodb_locks rl ON rl.lock_id = w.requested_lock_id\n ORDER BY r.trx_wait_started; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `r`.`trx_wait_started` AS `wait_started`,timediff(current_timestamp(),`r`.`trx_wait_started`) AS `wait_age`,timestampdiff(SECOND,`r`.`trx_wait_started`,current_timestamp()) AS `wait_age_secs`,`rl`.`lock_table` AS `locked_table`,`rl`.`lock_index` AS `locked_index`,`rl`.`lock_type` AS `locked_type`,`r`.`trx_id` AS `waiting_trx_id`,`r`.`trx_started` AS `waiting_trx_started`,timediff(current_timestamp(),`r`.`trx_started`) AS `waiting_trx_age`,`r`.`trx_rows_locked` AS `waiting_trx_rows_locked`,`r`.`trx_rows_modified` AS `waiting_trx_rows_modified`,`r`.`trx_mysql_thread_id` AS `waiting_pid`,`sys`.`format_statement`(`r`.`trx_query`) AS `waiting_query`,`rl`.`lock_id` AS `waiting_lock_id`,`rl`.`lock_mode` AS `waiting_lock_mode`,`b`.`trx_id` AS `blocking_trx_id`,`b`.`trx_mysql_thread_id` AS `blocking_pid`,`sys`.`format_statement`(`b`.`trx_query`) AS `blocking_query`,`bl`.`lock_id` AS `blocking_lock_id`,`bl`.`lock_mode` AS `blocking_lock_mode`,`b`.`trx_started` AS `blocking_trx_started`,timediff(current_timestamp(),`b`.`trx_started`) AS `blocking_trx_age`,`b`.`trx_rows_locked` AS `blocking_trx_rows_locked`,`b`.`trx_rows_modified` AS `blocking_trx_rows_modified`,concat(\'KILL QUERY \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_query`,concat(\'KILL \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_connection` from ((((`information_schema`.`innodb_lock_waits` `w` join `information_schema`.`innodb_trx` `b` on(`b`.`trx_id` = `w`.`blocking_trx_id`)) join `information_schema`.`innodb_trx` `r` on(`r`.`trx_id` = `w`.`requesting_trx_id`)) join `information_schema`.`innodb_locks` `bl` on(`bl`.`lock_id` = `w`.`blocking_lock_id`)) join `information_schema`.`innodb_locks` `rl` on(`rl`.`lock_id` = `w`.`requested_lock_id`)) order by `r`.`trx_wait_started` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/io_by_thread_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/io_by_thread_by_latency.frm new file mode 100644 index 0000000..4ed97c4 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/io_by_thread_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) AS `user`,sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(min(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MIN_TIMER_WAIT`)) AS `min_latency`,format_pico_time(avg(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`AVG_TIMER_WAIT`)) AS `avg_latency`,format_pico_time(max(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency`,`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` AS `thread_id`,`performance_schema`.`threads`.`PROCESSLIST_ID` AS `processlist_id` from (`performance_schema`.`events_waits_summary_by_thread_by_event_name` left join `performance_schema`.`threads` on(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) where `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT` > 0 group by `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID`,`performance_schema`.`threads`.`PROCESSLIST_ID`,if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) order by sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=fcc6525c06e21b5428f352f4bfcf37f0 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365158822 +create-version=2 +source=SELECT IF(processlist_id IS NULL,\n SUBSTRING_INDEX(name, \'/\', -1),\n CONCAT(processlist_user, \'@\', processlist_host)\n ) user,\n SUM(count_star) total,\n format_pico_time(SUM(sum_timer_wait)) total_latency,\n format_pico_time(MIN(min_timer_wait)) min_latency,\n format_pico_time(AVG(avg_timer_wait)) avg_latency,\n format_pico_time(MAX(max_timer_wait)) max_latency,\n thread_id,\n processlist_id\n FROM performance_schema.events_waits_summary_by_thread_by_event_name\n LEFT JOIN performance_schema.threads USING (thread_id)\n WHERE event_name LIKE \'wait/io/file/%\'\n AND sum_timer_wait > 0\n GROUP BY thread_id, processlist_id, user\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) AS `user`,sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(min(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MIN_TIMER_WAIT`)) AS `min_latency`,format_pico_time(avg(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`AVG_TIMER_WAIT`)) AS `avg_latency`,format_pico_time(max(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency`,`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` AS `thread_id`,`performance_schema`.`threads`.`PROCESSLIST_ID` AS `processlist_id` from (`performance_schema`.`events_waits_summary_by_thread_by_event_name` left join `performance_schema`.`threads` on(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) where `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT` > 0 group by `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID`,`performance_schema`.`threads`.`PROCESSLIST_ID`,if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) order by sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/io_global_by_file_by_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_file_by_bytes.frm new file mode 100644 index 0000000..54e3c7c --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_file_by_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_path`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_read`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_READ`,0),0)) AS `avg_read`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total_written`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE`,0),0.00)) AS `avg_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total`,ifnull(round(100 - `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`,0) * 100,2),0.00) AS `write_pct` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` desc +md5=a74849867ba5e2829284aa8d8254bd71 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365179362 +create-version=2 +source=SELECT sys.format_path(file_name) AS file,\n count_read,\n sys.format_bytes(sum_number_of_bytes_read) AS total_read,\n sys.format_bytes(IFNULL(sum_number_of_bytes_read / NULLIF(count_read, 0), 0)) AS avg_read,\n count_write,\n sys.format_bytes(sum_number_of_bytes_write) AS total_written,\n sys.format_bytes(IFNULL(sum_number_of_bytes_write / NULLIF(count_write, 0), 0.00)) AS avg_write,\n sys.format_bytes(sum_number_of_bytes_read + sum_number_of_bytes_write) AS total,\n IFNULL(ROUND(100-((sum_number_of_bytes_read/ NULLIF((sum_number_of_bytes_read+sum_number_of_bytes_write), 0))*100), 2), 0.00) AS write_pct\n FROM performance_schema.file_summary_by_instance\n ORDER BY sum_number_of_bytes_read + sum_number_of_bytes_write DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_path`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_read`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_READ`,0),0)) AS `avg_read`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total_written`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE`,0),0.00)) AS `avg_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total`,ifnull(round(100 - `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`,0) * 100,2),0.00) AS `write_pct` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/io_global_by_file_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_file_by_latency.frm new file mode 100644 index 0000000..c28d942 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_file_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_path`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_READ`) AS `read_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WRITE`) AS `write_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_MISC` AS `count_misc`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_MISC`) AS `misc_latency` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT` desc +md5=10e6e843da56650b0f41bda3755bf3a5 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365197368 +create-version=2 +source=SELECT sys.format_path(file_name) AS file,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n count_read,\n format_pico_time(sum_timer_read) AS read_latency,\n count_write,\n format_pico_time(sum_timer_write) AS write_latency,\n count_misc,\n format_pico_time(sum_timer_misc) AS misc_latency\n FROM performance_schema.file_summary_by_instance\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_path`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_READ`) AS `read_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WRITE`) AS `write_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_MISC` AS `count_misc`,format_pico_time(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_MISC`) AS `misc_latency` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/io_global_by_wait_by_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_wait_by_bytes.frm new file mode 100644 index 0000000..e9edccc --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_wait_by_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_read`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0)) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total_written`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0)) AS `avg_written`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_requested` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` desc +md5=7d930288769a3b345117191a1542af8e +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365219846 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name, \'/\', -2) event_name,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(min_timer_wait) AS min_latency,\n format_pico_time(avg_timer_wait) AS avg_latency,\n format_pico_time(max_timer_wait) AS max_latency,\n count_read,\n sys.format_bytes(sum_number_of_bytes_read) AS total_read,\n sys.format_bytes(IFNULL(sum_number_of_bytes_read / NULLIF(count_read, 0), 0)) AS avg_read,\n count_write,\n sys.format_bytes(sum_number_of_bytes_write) AS total_written,\n sys.format_bytes(IFNULL(sum_number_of_bytes_write / NULLIF(count_write, 0), 0)) AS avg_written,\n sys.format_bytes(sum_number_of_bytes_write + sum_number_of_bytes_read) AS total_requested\n FROM performance_schema.file_summary_by_event_name\n WHERE event_name LIKE \'wait/io/file/%\'\n AND count_star > 0\n ORDER BY sum_number_of_bytes_write + sum_number_of_bytes_read DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_read`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0)) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total_written`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0)) AS `avg_written`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_requested` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/io_global_by_wait_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_wait_by_latency.frm new file mode 100644 index 0000000..43dcaac --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/io_global_by_wait_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_READ`) AS `read_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WRITE`) AS `write_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_MISC`) AS `misc_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_read`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0)) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total_written`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0)) AS `avg_written` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` desc +md5=0272cfa47514b02ded601f6f9cf04fb0 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365238685 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name, \'/\', -2) AS event_name,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(avg_timer_wait) AS avg_latency,\n format_pico_time(max_timer_wait) AS max_latency,\n format_pico_time(sum_timer_read) AS read_latency,\n format_pico_time(sum_timer_write) AS write_latency,\n format_pico_time(sum_timer_misc) AS misc_latency,\n count_read,\n sys.format_bytes(sum_number_of_bytes_read) AS total_read,\n sys.format_bytes(IFNULL(sum_number_of_bytes_read / NULLIF(count_read, 0), 0)) AS avg_read,\n count_write,\n sys.format_bytes(sum_number_of_bytes_write) AS total_written,\n sys.format_bytes(IFNULL(sum_number_of_bytes_write / NULLIF(count_write, 0), 0)) AS avg_written\n FROM performance_schema.file_summary_by_event_name\n WHERE event_name LIKE \'wait/io/file/%\'\n AND count_star > 0\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_READ`) AS `read_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WRITE`) AS `write_latency`,format_pico_time(`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_MISC`) AS `misc_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ`) AS `total_read`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0)) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`sys`.`format_bytes`(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `total_written`,`sys`.`format_bytes`(ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0)) AS `avg_written` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/latest_file_io.frm b/src/main/docker-mariadb/mariadb_data/sys/latest_file_io.frm new file mode 100644 index 0000000..ccd6b1b --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/latest_file_io.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`information_schema`.`processlist`.`ID` is null,concat(substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),\':\',`performance_schema`.`events_waits_history_long`.`THREAD_ID`),concat(`information_schema`.`processlist`.`USER`,\'@\',`information_schema`.`processlist`.`HOST`,\':\',`information_schema`.`processlist`.`ID`)) AS `thread`,`sys`.`format_path`(`performance_schema`.`events_waits_history_long`.`OBJECT_NAME`) AS `file`,format_pico_time(`performance_schema`.`events_waits_history_long`.`TIMER_WAIT`) AS `latency`,`performance_schema`.`events_waits_history_long`.`OPERATION` AS `operation`,`sys`.`format_bytes`(`performance_schema`.`events_waits_history_long`.`NUMBER_OF_BYTES`) AS `requested` from ((`performance_schema`.`events_waits_history_long` join `performance_schema`.`threads` on(`performance_schema`.`events_waits_history_long`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) left join `information_schema`.`processlist` on(`performance_schema`.`threads`.`PROCESSLIST_ID` = `information_schema`.`processlist`.`ID`)) where `performance_schema`.`events_waits_history_long`.`OBJECT_NAME` is not null and `performance_schema`.`events_waits_history_long`.`EVENT_NAME` like \'wait/io/file/%\' order by `performance_schema`.`events_waits_history_long`.`TIMER_START` +md5=4f9e863887722ac7347d18738c55e5cb +updatable=0 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365139350 +create-version=2 +source=SELECT IF(id IS NULL,\n CONCAT(SUBSTRING_INDEX(name, \'/\', -1), \':\', thread_id),\n CONCAT(user, \'@\', host, \':\', id)\n ) thread,\n sys.format_path(object_name) file,\n format_pico_time(timer_wait) AS latency,\n operation,\n sys.format_bytes(number_of_bytes) AS requested\n FROM performance_schema.events_waits_history_long\n JOIN performance_schema.threads USING (thread_id)\n LEFT JOIN information_schema.processlist ON processlist_id = id\n WHERE object_name IS NOT NULL\n AND event_name LIKE \'wait/io/file/%\'\n ORDER BY timer_start; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`information_schema`.`processlist`.`ID` is null,concat(substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),\':\',`performance_schema`.`events_waits_history_long`.`THREAD_ID`),concat(`information_schema`.`processlist`.`USER`,\'@\',`information_schema`.`processlist`.`HOST`,\':\',`information_schema`.`processlist`.`ID`)) AS `thread`,`sys`.`format_path`(`performance_schema`.`events_waits_history_long`.`OBJECT_NAME`) AS `file`,format_pico_time(`performance_schema`.`events_waits_history_long`.`TIMER_WAIT`) AS `latency`,`performance_schema`.`events_waits_history_long`.`OPERATION` AS `operation`,`sys`.`format_bytes`(`performance_schema`.`events_waits_history_long`.`NUMBER_OF_BYTES`) AS `requested` from ((`performance_schema`.`events_waits_history_long` join `performance_schema`.`threads` on(`performance_schema`.`events_waits_history_long`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) left join `information_schema`.`processlist` on(`performance_schema`.`threads`.`PROCESSLIST_ID` = `information_schema`.`processlist`.`ID`)) where `performance_schema`.`events_waits_history_long`.`OBJECT_NAME` is not null and `performance_schema`.`events_waits_history_long`.`EVENT_NAME` like \'wait/io/file/%\' order by `performance_schema`.`events_waits_history_long`.`TIMER_START` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/memory_by_host_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/memory_by_host_by_current_bytes.frm new file mode 100644 index 0000000..dfbc775 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/memory_by_host_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_allocated`,`sys`.`format_bytes`(ifnull(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`),0),0)) AS `current_avg_alloc`,`sys`.`format_bytes`(max(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_max_alloc`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`)) AS `total_allocated` from `performance_schema`.`memory_summary_by_host_by_event_name` group by if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +md5=93c91e80e1a600b69b90e1a671a15ed6 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365280860 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n SUM(current_count_used) AS current_count_used,\n sys.format_bytes(SUM(current_number_of_bytes_used)) AS current_allocated,\n sys.format_bytes(IFNULL(SUM(current_number_of_bytes_used) / NULLIF(SUM(current_count_used), 0), 0)) AS current_avg_alloc,\n sys.format_bytes(MAX(current_number_of_bytes_used)) AS current_max_alloc,\n sys.format_bytes(SUM(sum_number_of_bytes_alloc)) AS total_allocated\n FROM performance_schema.memory_summary_by_host_by_event_name\n GROUP BY IF(host IS NULL, \'background\', host)\n ORDER BY SUM(current_number_of_bytes_used) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_allocated`,`sys`.`format_bytes`(ifnull(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`),0),0)) AS `current_avg_alloc`,`sys`.`format_bytes`(max(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_max_alloc`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`)) AS `total_allocated` from `performance_schema`.`memory_summary_by_host_by_event_name` group by if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/memory_by_thread_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/memory_by_thread_by_current_bytes.frm new file mode 100644 index 0000000..4bfc8ba --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/memory_by_thread_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `mt`.`THREAD_ID` AS `thread_id`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) AS `user`,sum(`mt`.`CURRENT_COUNT_USED`) AS `current_count_used`,`sys`.`format_bytes`(sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_allocated`,`sys`.`format_bytes`(ifnull(sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`mt`.`CURRENT_COUNT_USED`),0),0)) AS `current_avg_alloc`,`sys`.`format_bytes`(max(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_max_alloc`,`sys`.`format_bytes`(sum(`mt`.`SUM_NUMBER_OF_BYTES_ALLOC`)) AS `total_allocated` from (`performance_schema`.`memory_summary_by_thread_by_event_name` `mt` join `performance_schema`.`threads` `t` on(`mt`.`THREAD_ID` = `t`.`THREAD_ID`)) group by `mt`.`THREAD_ID`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) order by sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +md5=bae940445aa2189841198ee54ad4ff09 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365302084 +create-version=2 +source=SELECT thread_id,\n IF(t.name = \'thread/sql/one_connection\',\n CONCAT(t.processlist_user, \'@\', t.processlist_host),\n REPLACE(t.name, \'thread/\', \'\')) user,\n SUM(mt.current_count_used) AS current_count_used,\n sys.format_bytes(SUM(mt.current_number_of_bytes_used)) AS current_allocated,\n sys.format_bytes(IFNULL(SUM(mt.current_number_of_bytes_used) / NULLIF(SUM(current_count_used), 0), 0)) AS current_avg_alloc,\n sys.format_bytes(MAX(mt.current_number_of_bytes_used)) AS current_max_alloc,\n sys.format_bytes(SUM(mt.sum_number_of_bytes_alloc)) AS total_allocated\n FROM performance_schema.memory_summary_by_thread_by_event_name AS mt\n JOIN performance_schema.threads AS t USING (thread_id)\n GROUP BY thread_id, IF(t.name = \'thread/sql/one_connection\',\n CONCAT(t.processlist_user, \'@\', t.processlist_host),\n REPLACE(t.name, \'thread/\', \'\'))\n ORDER BY SUM(current_number_of_bytes_used) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `mt`.`THREAD_ID` AS `thread_id`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) AS `user`,sum(`mt`.`CURRENT_COUNT_USED`) AS `current_count_used`,`sys`.`format_bytes`(sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_allocated`,`sys`.`format_bytes`(ifnull(sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`mt`.`CURRENT_COUNT_USED`),0),0)) AS `current_avg_alloc`,`sys`.`format_bytes`(max(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_max_alloc`,`sys`.`format_bytes`(sum(`mt`.`SUM_NUMBER_OF_BYTES_ALLOC`)) AS `total_allocated` from (`performance_schema`.`memory_summary_by_thread_by_event_name` `mt` join `performance_schema`.`threads` `t` on(`mt`.`THREAD_ID` = `t`.`THREAD_ID`)) group by `mt`.`THREAD_ID`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) order by sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/memory_by_user_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/memory_by_user_by_current_bytes.frm new file mode 100644 index 0000000..0640f04 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/memory_by_user_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_allocated`,`sys`.`format_bytes`(ifnull(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`),0),0)) AS `current_avg_alloc`,`sys`.`format_bytes`(max(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_max_alloc`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`)) AS `total_allocated` from `performance_schema`.`memory_summary_by_user_by_event_name` group by if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +md5=f057d6d83c301f761890986ff9b2a9a2 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365259950 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUM(current_count_used) AS current_count_used,\n sys.format_bytes(SUM(current_number_of_bytes_used)) AS current_allocated,\n sys.format_bytes(IFNULL(SUM(current_number_of_bytes_used) / NULLIF(SUM(current_count_used), 0), 0)) AS current_avg_alloc,\n sys.format_bytes(MAX(current_number_of_bytes_used)) AS current_max_alloc,\n sys.format_bytes(SUM(sum_number_of_bytes_alloc)) AS total_allocated\n FROM performance_schema.memory_summary_by_user_by_event_name\n GROUP BY IF(user IS NULL, \'background\', user)\n ORDER BY SUM(current_number_of_bytes_used) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_allocated`,`sys`.`format_bytes`(ifnull(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`),0),0)) AS `current_avg_alloc`,`sys`.`format_bytes`(max(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `current_max_alloc`,`sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`)) AS `total_allocated` from `performance_schema`.`memory_summary_by_user_by_event_name` group by if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/memory_global_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/memory_global_by_current_bytes.frm new file mode 100644 index 0000000..a5ca989 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/memory_global_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`memory_summary_global_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED` AS `current_count`,`sys`.`format_bytes`(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_alloc`,`sys`.`format_bytes`(ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED`,0),0)) AS `current_avg_alloc`,`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED` AS `high_count`,`sys`.`format_bytes`(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED`) AS `high_alloc`,`sys`.`format_bytes`(ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED`,0),0)) AS `high_avg_alloc` from `performance_schema`.`memory_summary_global_by_event_name` where `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` > 0 order by `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` desc +md5=e5638aa4612faf9c663f68742eb47175 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365322314 +create-version=2 +source=SELECT event_name,\n current_count_used AS current_count,\n sys.format_bytes(current_number_of_bytes_used) AS current_alloc,\n sys.format_bytes(IFNULL(current_number_of_bytes_used / NULLIF(current_count_used, 0), 0)) AS current_avg_alloc,\n high_count_used AS high_count,\n sys.format_bytes(high_number_of_bytes_used) AS high_alloc,\n sys.format_bytes(IFNULL(high_number_of_bytes_used / NULLIF(high_count_used, 0), 0)) AS high_avg_alloc\n FROM performance_schema.memory_summary_global_by_event_name\n WHERE current_number_of_bytes_used > 0\n ORDER BY current_number_of_bytes_used DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`memory_summary_global_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED` AS `current_count`,`sys`.`format_bytes`(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_alloc`,`sys`.`format_bytes`(ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED`,0),0)) AS `current_avg_alloc`,`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED` AS `high_count`,`sys`.`format_bytes`(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED`) AS `high_alloc`,`sys`.`format_bytes`(ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED`,0),0)) AS `high_avg_alloc` from `performance_schema`.`memory_summary_global_by_event_name` where `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` > 0 order by `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/memory_global_total.frm b/src/main/docker-mariadb/mariadb_data/sys/memory_global_total.frm new file mode 100644 index 0000000..a0f2aef --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/memory_global_total.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `total_allocated` from `performance_schema`.`memory_summary_global_by_event_name` +md5=8082fddb38d6165c0d33b88815ddf3d8 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365340117 +create-version=2 +source=SELECT sys.format_bytes(SUM(CURRENT_NUMBER_OF_BYTES_USED)) total_allocated\n FROM performance_schema.memory_summary_global_by_event_name; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_bytes`(sum(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`)) AS `total_allocated` from `performance_schema`.`memory_summary_global_by_event_name` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/metrics.frm b/src/main/docker-mariadb/mariadb_data/sys/metrics.frm new file mode 100644 index 0000000..9787b78 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/metrics.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=(select lcase(`performance_schema`.`global_status`.`VARIABLE_NAME`) AS `Variable_name`,`performance_schema`.`global_status`.`VARIABLE_VALUE` AS `Variable_value`,\'Global Status\' AS `Type`,\'YES\' AS `Enabled` from `performance_schema`.`global_status`) union all (select `information_schema`.`innodb_metrics`.`NAME` AS `Variable_name`,`information_schema`.`innodb_metrics`.`COUNT` AS `Variable_value`,concat(\'InnoDB Metrics - \',`information_schema`.`innodb_metrics`.`SUBSYSTEM`) AS `Type`,\'YES\' AS `Enabled` from `information_schema`.`innodb_metrics` where `information_schema`.`innodb_metrics`.`NAME` not in (\'lock_row_lock_time\',\'lock_row_lock_time_avg\',\'lock_row_lock_time_max\',\'lock_row_lock_waits\',\'buffer_pool_reads\',\'buffer_pool_read_requests\',\'buffer_pool_write_requests\',\'buffer_pool_wait_free\',\'buffer_pool_read_ahead\',\'buffer_pool_read_ahead_evicted\',\'buffer_pool_pages_total\',\'buffer_pool_pages_misc\',\'buffer_pool_pages_data\',\'buffer_pool_bytes_data\',\'buffer_pool_pages_dirty\',\'buffer_pool_bytes_dirty\',\'buffer_pool_pages_free\',\'buffer_pages_created\',\'buffer_pages_written\',\'buffer_pages_read\',\'buffer_data_reads\',\'buffer_data_written\',\'file_num_open_files\',\'os_log_bytes_written\',\'os_log_fsyncs\',\'os_log_pending_fsyncs\',\'os_log_pending_writes\',\'log_waits\',\'log_write_requests\',\'log_writes\',\'innodb_dblwr_writes\',\'innodb_dblwr_pages_written\',\'innodb_page_size\')) union all (select \'NOW()\' AS `Variable_name`,current_timestamp(3) AS `Variable_value`,\'System Time\' AS `Type`,\'YES\' AS `Enabled`) union all (select \'UNIX_TIMESTAMP()\' AS `Variable_name`,round(unix_timestamp(current_timestamp(3)),3) AS `Variable_value`,\'System Time\' AS `Type`,\'YES\' AS `Enabled`) order by `Type`,`Variable_name` +md5=4eec971a353c3babe46668653b2280ff +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365993652 +create-version=2 +source=(\nSELECT LOWER(VARIABLE_NAME) AS Variable_name, VARIABLE_VALUE AS Variable_value, \'Global Status\' AS Type, \'YES\' AS Enabled\n FROM performance_schema.global_status\n) UNION ALL (\nSELECT NAME AS Variable_name, COUNT AS Variable_value,\n CONCAT(\'InnoDB Metrics - \', SUBSYSTEM) AS Type,\n \'YES\' AS Enabled\n FROM information_schema.INNODB_METRICS\n WHERE NAME NOT IN (\n \'lock_row_lock_time\', \'lock_row_lock_time_avg\', \'lock_row_lock_time_max\', \'lock_row_lock_waits\',\n \'buffer_pool_reads\', \'buffer_pool_read_requests\', \'buffer_pool_write_requests\', \'buffer_pool_wait_free\',\n \'buffer_pool_read_ahead\', \'buffer_pool_read_ahead_evicted\', \'buffer_pool_pages_total\', \'buffer_pool_pages_misc\',\n \'buffer_pool_pages_data\', \'buffer_pool_bytes_data\', \'buffer_pool_pages_dirty\', \'buffer_pool_bytes_dirty\',\n \'buffer_pool_pages_free\', \'buffer_pages_created\', \'buffer_pages_written\', \'buffer_pages_read\',\n \'buffer_data_reads\', \'buffer_data_written\', \'file_num_open_files\',\n \'os_log_bytes_written\', \'os_log_fsyncs\', \'os_log_pending_fsyncs\', \'os_log_pending_writes\',\n \'log_waits\', \'log_write_requests\', \'log_writes\', \'innodb_dblwr_writes\', \'innodb_dblwr_pages_written\', \'innodb_page_size\')\n) \n UNION ALL (\nSELECT \'NOW()\' AS Variable_name, NOW(3) AS Variable_value, \'System Time\' AS Type, \'YES\' AS Enabled\n) UNION ALL (\nSELECT \'UNIX_TIMESTAMP()\' AS Variable_name, ROUND(UNIX_TIMESTAMP(NOW(3)), 3) AS Variable_value, \'System Time\' AS Type, \'YES\' AS Enabled\n)\n ORDER BY Type, Variable_name; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=(select lcase(`performance_schema`.`global_status`.`VARIABLE_NAME`) AS `Variable_name`,`performance_schema`.`global_status`.`VARIABLE_VALUE` AS `Variable_value`,\'Global Status\' AS `Type`,\'YES\' AS `Enabled` from `performance_schema`.`global_status`) union all (select `information_schema`.`innodb_metrics`.`NAME` AS `Variable_name`,`information_schema`.`innodb_metrics`.`COUNT` AS `Variable_value`,concat(\'InnoDB Metrics - \',`information_schema`.`innodb_metrics`.`SUBSYSTEM`) AS `Type`,\'YES\' AS `Enabled` from `information_schema`.`innodb_metrics` where `information_schema`.`innodb_metrics`.`NAME` not in (\'lock_row_lock_time\',\'lock_row_lock_time_avg\',\'lock_row_lock_time_max\',\'lock_row_lock_waits\',\'buffer_pool_reads\',\'buffer_pool_read_requests\',\'buffer_pool_write_requests\',\'buffer_pool_wait_free\',\'buffer_pool_read_ahead\',\'buffer_pool_read_ahead_evicted\',\'buffer_pool_pages_total\',\'buffer_pool_pages_misc\',\'buffer_pool_pages_data\',\'buffer_pool_bytes_data\',\'buffer_pool_pages_dirty\',\'buffer_pool_bytes_dirty\',\'buffer_pool_pages_free\',\'buffer_pages_created\',\'buffer_pages_written\',\'buffer_pages_read\',\'buffer_data_reads\',\'buffer_data_written\',\'file_num_open_files\',\'os_log_bytes_written\',\'os_log_fsyncs\',\'os_log_pending_fsyncs\',\'os_log_pending_writes\',\'log_waits\',\'log_write_requests\',\'log_writes\',\'innodb_dblwr_writes\',\'innodb_dblwr_pages_written\',\'innodb_page_size\')) union all (select \'NOW()\' AS `Variable_name`,current_timestamp(3) AS `Variable_value`,\'System Time\' AS `Type`,\'YES\' AS `Enabled`) union all (select \'UNIX_TIMESTAMP()\' AS `Variable_name`,round(unix_timestamp(current_timestamp(3)),3) AS `Variable_value`,\'System Time\' AS `Type`,\'YES\' AS `Enabled`) order by `Type`,`Variable_name` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/privileges_by_table_by_level.frm b/src/main/docker-mariadb/mariadb_data/sys/privileges_by_table_by_level.frm new file mode 100644 index 0000000..41bfc82 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/privileges_by_table_by_level.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `t`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,`t`.`TABLE_NAME` AS `TABLE_NAME`,`privs`.`GRANTEE` AS `GRANTEE`,`privs`.`PRIVILEGE_TYPE` AS `PRIVILEGE`,`privs`.`LEVEL` AS `LEVEL` from (`information_schema`.`tables` `t` join (select NULL AS `TABLE_SCHEMA`,NULL AS `TABLE_NAME`,`information_schema`.`user_privileges`.`GRANTEE` AS `GRANTEE`,`information_schema`.`user_privileges`.`PRIVILEGE_TYPE` AS `PRIVILEGE_TYPE`,\'GLOBAL\' AS `LEVEL` from `information_schema`.`user_privileges` union select `information_schema`.`schema_privileges`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,NULL AS `TABLE_NAME`,`information_schema`.`schema_privileges`.`GRANTEE` AS `GRANTEE`,`information_schema`.`schema_privileges`.`PRIVILEGE_TYPE` AS `PRIVILEGE_TYPE`,\'SCHEMA\' AS `LEVEL` from `information_schema`.`schema_privileges` union select `information_schema`.`table_privileges`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,`information_schema`.`table_privileges`.`TABLE_NAME` AS `TABLE_NAME`,`information_schema`.`table_privileges`.`GRANTEE` AS `GRANTEE`,`information_schema`.`table_privileges`.`PRIVILEGE_TYPE` AS `PRIVILEGE_TYPE`,\'TABLE\' AS `LEVEL` from `information_schema`.`table_privileges`) `privs` on((`t`.`TABLE_SCHEMA` = `privs`.`TABLE_SCHEMA` or `privs`.`TABLE_SCHEMA` is null) and (`t`.`TABLE_NAME` = `privs`.`TABLE_NAME` or `privs`.`TABLE_NAME` is null) and `privs`.`PRIVILEGE_TYPE` in (\'SELECT\',\'INSERT\',\'UPDATE\',\'DELETE\',\'CREATE\',\'ALTER\',\'DROP\',\'INDEX\',\'REFERENCES\',\'TRIGGER\',\'GRANT OPTION\',\'SHOW VIEW\',\'DELETE HISTORY\'))) where `t`.`TABLE_SCHEMA` not in (\'sys\',\'mysql\',\'information_schema\',\'performance_schema\') +md5=4e8934e239a14d16ba65d253021e392d +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365113794 +create-version=2 +source=SELECT t.TABLE_SCHEMA,\n t.TABLE_NAME,\n privs.GRANTEE,\n privs.PRIVILEGE_TYPE,\n privs.LEVEL\nFROM INFORMATION_SCHEMA.TABLES AS t\nJOIN ( SELECT NULL AS TABLE_SCHEMA,\n NULL AS TABLE_NAME,\n GRANTEE,\n PRIVILEGE_TYPE,\n \'GLOBAL\' LEVEL\n FROM INFORMATION_SCHEMA.USER_PRIVILEGES\n UNION\n SELECT TABLE_SCHEMA,\n NULL AS TABLE_NAME,\n GRANTEE,\n PRIVILEGE_TYPE,\n \'SCHEMA\' LEVEL\n FROM INFORMATION_SCHEMA.SCHEMA_PRIVILEGES\n UNION\n SELECT TABLE_SCHEMA,\n TABLE_NAME,\n GRANTEE,\n PRIVILEGE_TYPE,\n \'TABLE\' LEVEL\n FROM INFORMATION_SCHEMA.TABLE_PRIVILEGES\n ) privs\n ON (t.TABLE_SCHEMA = privs.TABLE_SCHEMA OR privs.TABLE_SCHEMA IS NULL)\n AND (t.TABLE_NAME = privs.TABLE_NAME OR privs.TABLE_NAME IS NULL)\n AND privs.PRIVILEGE_TYPE IN (\'SELECT\', \'INSERT\', \'UPDATE\', \'DELETE\',\n \'CREATE\', \'ALTER\', \'DROP\', \'INDEX\',\n \'REFERENCES\', \'TRIGGER\', \'GRANT OPTION\',\n \'SHOW VIEW\', \'DELETE HISTORY\')\nWHERE t.TABLE_SCHEMA NOT IN (\'sys\', \'mysql\',\'information_schema\',\n \'performance_schema\'); +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `t`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,`t`.`TABLE_NAME` AS `TABLE_NAME`,`privs`.`GRANTEE` AS `GRANTEE`,`privs`.`PRIVILEGE_TYPE` AS `PRIVILEGE`,`privs`.`LEVEL` AS `LEVEL` from (`information_schema`.`tables` `t` join (select NULL AS `TABLE_SCHEMA`,NULL AS `TABLE_NAME`,`information_schema`.`user_privileges`.`GRANTEE` AS `GRANTEE`,`information_schema`.`user_privileges`.`PRIVILEGE_TYPE` AS `PRIVILEGE_TYPE`,\'GLOBAL\' AS `LEVEL` from `information_schema`.`user_privileges` union select `information_schema`.`schema_privileges`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,NULL AS `TABLE_NAME`,`information_schema`.`schema_privileges`.`GRANTEE` AS `GRANTEE`,`information_schema`.`schema_privileges`.`PRIVILEGE_TYPE` AS `PRIVILEGE_TYPE`,\'SCHEMA\' AS `LEVEL` from `information_schema`.`schema_privileges` union select `information_schema`.`table_privileges`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,`information_schema`.`table_privileges`.`TABLE_NAME` AS `TABLE_NAME`,`information_schema`.`table_privileges`.`GRANTEE` AS `GRANTEE`,`information_schema`.`table_privileges`.`PRIVILEGE_TYPE` AS `PRIVILEGE_TYPE`,\'TABLE\' AS `LEVEL` from `information_schema`.`table_privileges`) `privs` on((`t`.`TABLE_SCHEMA` = `privs`.`TABLE_SCHEMA` or `privs`.`TABLE_SCHEMA` is null) and (`t`.`TABLE_NAME` = `privs`.`TABLE_NAME` or `privs`.`TABLE_NAME` is null) and `privs`.`PRIVILEGE_TYPE` in (\'SELECT\',\'INSERT\',\'UPDATE\',\'DELETE\',\'CREATE\',\'ALTER\',\'DROP\',\'INDEX\',\'REFERENCES\',\'TRIGGER\',\'GRANT OPTION\',\'SHOW VIEW\',\'DELETE HISTORY\'))) where `t`.`TABLE_SCHEMA` not in (\'sys\',\'mysql\',\'information_schema\',\'performance_schema\') +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/processlist.frm b/src/main/docker-mariadb/mariadb_data/sys/processlist.frm new file mode 100644 index 0000000..fc99c28 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/processlist.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `pps`.`THREAD_ID` AS `thd_id`,`pps`.`PROCESSLIST_ID` AS `conn_id`,if(`pps`.`NAME` = \'thread/sql/one_connection\',concat(`pps`.`PROCESSLIST_USER`,\'@\',`pps`.`PROCESSLIST_HOST`),replace(`pps`.`NAME`,\'thread/\',\'\')) AS `user`,`pps`.`PROCESSLIST_DB` AS `db`,`pps`.`PROCESSLIST_COMMAND` AS `command`,`pps`.`PROCESSLIST_STATE` AS `state`,`pps`.`PROCESSLIST_TIME` AS `time`,`sys`.`format_statement`(`pps`.`PROCESSLIST_INFO`) AS `current_statement`,if(`esc`.`END_EVENT_ID` is null,format_pico_time(`esc`.`TIMER_WAIT`),NULL) AS `statement_latency`,if(`esc`.`END_EVENT_ID` is null,round(100 * (`estc`.`WORK_COMPLETED` / `estc`.`WORK_ESTIMATED`),2),NULL) AS `progress`,format_pico_time(`esc`.`LOCK_TIME`) AS `lock_latency`,`esc`.`ROWS_EXAMINED` AS `rows_examined`,`esc`.`ROWS_SENT` AS `rows_sent`,`esc`.`ROWS_AFFECTED` AS `rows_affected`,`esc`.`CREATED_TMP_TABLES` AS `tmp_tables`,`esc`.`CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,if(`esc`.`NO_GOOD_INDEX_USED` > 0 or `esc`.`NO_INDEX_USED` > 0,\'YES\',\'NO\') AS `full_scan`,if(`esc`.`END_EVENT_ID` is not null,`sys`.`format_statement`(`esc`.`SQL_TEXT`),NULL) AS `last_statement`,if(`esc`.`END_EVENT_ID` is not null,format_pico_time(`esc`.`TIMER_WAIT`),NULL) AS `last_statement_latency`,`sys`.`format_bytes`(`mem`.`current_allocated`) AS `current_memory`,`ewc`.`EVENT_NAME` AS `last_wait`,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',format_pico_time(`ewc`.`TIMER_WAIT`)) AS `last_wait_latency`,`ewc`.`SOURCE` AS `source`,format_pico_time(`etc`.`TIMER_WAIT`) AS `trx_latency`,`etc`.`STATE` AS `trx_state`,`etc`.`AUTOCOMMIT` AS `trx_autocommit`,`conattr_pid`.`ATTR_VALUE` AS `pid`,`conattr_progname`.`ATTR_VALUE` AS `program_name` from (((((((`performance_schema`.`threads` `pps` left join `performance_schema`.`events_waits_current` `ewc` on(`pps`.`THREAD_ID` = `ewc`.`THREAD_ID`)) left join `performance_schema`.`events_stages_current` `estc` on(`pps`.`THREAD_ID` = `estc`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `esc` on(`pps`.`THREAD_ID` = `esc`.`THREAD_ID`)) left join `performance_schema`.`events_transactions_current` `etc` on(`pps`.`THREAD_ID` = `etc`.`THREAD_ID`)) left join `sys`.`x$memory_by_thread_by_current_bytes` `mem` on(`pps`.`THREAD_ID` = `mem`.`thread_id`)) left join `performance_schema`.`session_connect_attrs` `conattr_pid` on(`conattr_pid`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_pid`.`ATTR_NAME` = \'_pid\')) left join `performance_schema`.`session_connect_attrs` `conattr_progname` on(`conattr_progname`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_progname`.`ATTR_NAME` = \'program_name\')) order by `pps`.`PROCESSLIST_TIME` desc,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',format_pico_time(`ewc`.`TIMER_WAIT`)) desc +md5=83a8094a609aeaecda720b5ff342da2f +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298366011761 +create-version=2 +source=SELECT pps.thread_id AS thd_id,\n pps.processlist_id AS conn_id,\n IF(pps.name = \'thread/sql/one_connection\',\n CONCAT(pps.processlist_user, \'@\', pps.processlist_host),\n REPLACE(pps.name, \'thread/\', \'\')) user,\n pps.processlist_db AS db,\n pps.processlist_command AS command,\n pps.processlist_state AS state,\n pps.processlist_time AS time,\n sys.format_statement(pps.processlist_info) AS current_statement,\n IF(esc.end_event_id IS NULL,\n format_pico_time(esc.timer_wait),\n NULL) AS statement_latency,\n IF(esc.end_event_id IS NULL,\n ROUND(100 * (estc.work_completed / estc.work_estimated), 2),\n NULL) AS progress,\n format_pico_time(esc.lock_time) AS lock_latency,\n esc.rows_examined AS rows_examined,\n esc.rows_sent AS rows_sent,\n esc.rows_affected AS rows_affected,\n esc.created_tmp_tables AS tmp_tables,\n esc.created_tmp_disk_tables AS tmp_disk_tables,\n IF(esc.no_good_index_used > 0 OR esc.no_index_used > 0, \'YES\', \'NO\') AS full_scan,\n IF(esc.end_event_id IS NOT NULL,\n sys.format_statement(esc.sql_text),\n NULL) AS last_statement,\n IF(esc.end_event_id IS NOT NULL,\n format_pico_time(esc.timer_wait),\n NULL) AS last_statement_latency,\n sys.format_bytes(mem.current_allocated) AS current_memory,\n ewc.event_name AS last_wait,\n IF(ewc.end_event_id IS NULL AND ewc.event_name IS NOT NULL,\n \'Still Waiting\',\n format_pico_time(ewc.timer_wait)) last_wait_latency,\n ewc.source,\n format_pico_time(etc.timer_wait) AS trx_latency,\n etc.state AS trx_state,\n etc.autocommit AS trx_autocommit,\n conattr_pid.attr_value as pid,\n conattr_progname.attr_value as program_name\n FROM performance_schema.threads AS pps\n LEFT JOIN performance_schema.events_waits_current AS ewc USING (thread_id)\n LEFT JOIN performance_schema.events_stages_current AS estc USING (thread_id)\n LEFT JOIN performance_schema.events_statements_current AS esc USING (thread_id)\n LEFT JOIN performance_schema.events_transactions_current AS etc USING (thread_id)\n LEFT JOIN sys.x$memory_by_thread_by_current_bytes AS mem USING (thread_id)\n LEFT JOIN performance_schema.session_connect_attrs AS conattr_pid\n ON conattr_pid.processlist_id=pps.processlist_id and conattr_pid.attr_name=\'_pid\'\n LEFT JOIN performance_schema.session_connect_attrs AS conattr_progname\n ON conattr_progname.processlist_id=pps.processlist_id and conattr_progname.attr_name=\'program_name\'\n ORDER BY pps.processlist_time DESC, last_wait_latency DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `pps`.`THREAD_ID` AS `thd_id`,`pps`.`PROCESSLIST_ID` AS `conn_id`,if(`pps`.`NAME` = \'thread/sql/one_connection\',concat(`pps`.`PROCESSLIST_USER`,\'@\',`pps`.`PROCESSLIST_HOST`),replace(`pps`.`NAME`,\'thread/\',\'\')) AS `user`,`pps`.`PROCESSLIST_DB` AS `db`,`pps`.`PROCESSLIST_COMMAND` AS `command`,`pps`.`PROCESSLIST_STATE` AS `state`,`pps`.`PROCESSLIST_TIME` AS `time`,`sys`.`format_statement`(`pps`.`PROCESSLIST_INFO`) AS `current_statement`,if(`esc`.`END_EVENT_ID` is null,format_pico_time(`esc`.`TIMER_WAIT`),NULL) AS `statement_latency`,if(`esc`.`END_EVENT_ID` is null,round(100 * (`estc`.`WORK_COMPLETED` / `estc`.`WORK_ESTIMATED`),2),NULL) AS `progress`,format_pico_time(`esc`.`LOCK_TIME`) AS `lock_latency`,`esc`.`ROWS_EXAMINED` AS `rows_examined`,`esc`.`ROWS_SENT` AS `rows_sent`,`esc`.`ROWS_AFFECTED` AS `rows_affected`,`esc`.`CREATED_TMP_TABLES` AS `tmp_tables`,`esc`.`CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,if(`esc`.`NO_GOOD_INDEX_USED` > 0 or `esc`.`NO_INDEX_USED` > 0,\'YES\',\'NO\') AS `full_scan`,if(`esc`.`END_EVENT_ID` is not null,`sys`.`format_statement`(`esc`.`SQL_TEXT`),NULL) AS `last_statement`,if(`esc`.`END_EVENT_ID` is not null,format_pico_time(`esc`.`TIMER_WAIT`),NULL) AS `last_statement_latency`,`sys`.`format_bytes`(`mem`.`current_allocated`) AS `current_memory`,`ewc`.`EVENT_NAME` AS `last_wait`,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',format_pico_time(`ewc`.`TIMER_WAIT`)) AS `last_wait_latency`,`ewc`.`SOURCE` AS `source`,format_pico_time(`etc`.`TIMER_WAIT`) AS `trx_latency`,`etc`.`STATE` AS `trx_state`,`etc`.`AUTOCOMMIT` AS `trx_autocommit`,`conattr_pid`.`ATTR_VALUE` AS `pid`,`conattr_progname`.`ATTR_VALUE` AS `program_name` from (((((((`performance_schema`.`threads` `pps` left join `performance_schema`.`events_waits_current` `ewc` on(`pps`.`THREAD_ID` = `ewc`.`THREAD_ID`)) left join `performance_schema`.`events_stages_current` `estc` on(`pps`.`THREAD_ID` = `estc`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `esc` on(`pps`.`THREAD_ID` = `esc`.`THREAD_ID`)) left join `performance_schema`.`events_transactions_current` `etc` on(`pps`.`THREAD_ID` = `etc`.`THREAD_ID`)) left join `sys`.`x$memory_by_thread_by_current_bytes` `mem` on(`pps`.`THREAD_ID` = `mem`.`thread_id`)) left join `performance_schema`.`session_connect_attrs` `conattr_pid` on(`conattr_pid`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_pid`.`ATTR_NAME` = \'_pid\')) left join `performance_schema`.`session_connect_attrs` `conattr_progname` on(`conattr_progname`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_progname`.`ATTR_NAME` = \'program_name\')) order by `pps`.`PROCESSLIST_TIME` desc,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',format_pico_time(`ewc`.`TIMER_WAIT`)) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/ps_check_lost_instrumentation.frm b/src/main/docker-mariadb/mariadb_data/sys/ps_check_lost_instrumentation.frm new file mode 100644 index 0000000..d931007 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/ps_check_lost_instrumentation.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`global_status`.`VARIABLE_NAME` AS `variable_name`,`performance_schema`.`global_status`.`VARIABLE_VALUE` AS `variable_value` from `performance_schema`.`global_status` where `performance_schema`.`global_status`.`VARIABLE_NAME` like \'perf%lost\' and `performance_schema`.`global_status`.`VARIABLE_VALUE` > 0 +md5=c734b24ae48c36b59fc217e2407acb24 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365127038 +create-version=2 +source=SELECT variable_name, variable_value\n FROM performance_schema.global_status\n WHERE variable_name LIKE \'perf%lost\'\n AND variable_value > 0; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`global_status`.`VARIABLE_NAME` AS `variable_name`,`performance_schema`.`global_status`.`VARIABLE_VALUE` AS `variable_value` from `performance_schema`.`global_status` where `performance_schema`.`global_status`.`VARIABLE_NAME` like \'perf%lost\' and `performance_schema`.`global_status`.`VARIABLE_VALUE` > 0 +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_auto_increment_columns.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_auto_increment_columns.frm new file mode 100644 index 0000000..d57cccf --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_auto_increment_columns.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `information_schema`.`columns`.`TABLE_SCHEMA` AS `table_schema`,`information_schema`.`columns`.`TABLE_NAME` AS `table_name`,`information_schema`.`columns`.`COLUMN_NAME` AS `column_name`,`information_schema`.`columns`.`DATA_TYPE` AS `data_type`,`information_schema`.`columns`.`COLUMN_TYPE` AS `column_type`,locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) = 0 AS `is_signed`,locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0 AS `is_unsigned`,case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1) AS `max_value`,`information_schema`.`tables`.`AUTO_INCREMENT` AS `auto_increment`,`information_schema`.`tables`.`AUTO_INCREMENT` / (case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1)) AS `auto_increment_ratio` from (`information_schema`.`columns` join `information_schema`.`tables` on(`information_schema`.`columns`.`TABLE_SCHEMA` = `information_schema`.`tables`.`TABLE_SCHEMA` and `information_schema`.`columns`.`TABLE_NAME` = `information_schema`.`tables`.`TABLE_NAME`)) where `information_schema`.`columns`.`TABLE_SCHEMA` not in (\'mysql\',\'sys\',\'INFORMATION_SCHEMA\',\'performance_schema\') and `information_schema`.`tables`.`TABLE_TYPE` = \'BASE TABLE\' and `information_schema`.`columns`.`EXTRA` = \'auto_increment\' order by `information_schema`.`tables`.`AUTO_INCREMENT` / (case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1)) desc,case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1) +md5=149e162c15ae1a0cb14a7ca374e833f2 +updatable=0 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365072427 +create-version=2 +source=SELECT TABLE_SCHEMA,\n TABLE_NAME,\n COLUMN_NAME,\n DATA_TYPE,\n COLUMN_TYPE,\n (LOCATE(\'unsigned\', COLUMN_TYPE) = 0) AS is_signed,\n (LOCATE(\'unsigned\', COLUMN_TYPE) > 0) AS is_unsigned,\n (\n CASE DATA_TYPE\n WHEN \'tinyint\' THEN 255\n WHEN \'smallint\' THEN 65535\n WHEN \'mediumint\' THEN 16777215\n WHEN \'int\' THEN 4294967295\n WHEN \'bigint\' THEN 18446744073709551615\n END >> IF(LOCATE(\'unsigned\', COLUMN_TYPE) > 0, 0, 1)\n ) AS max_value,\n AUTO_INCREMENT,\n AUTO_INCREMENT / (\n CASE DATA_TYPE\n WHEN \'tinyint\' THEN 255\n WHEN \'smallint\' THEN 65535\n WHEN \'mediumint\' THEN 16777215\n WHEN \'int\' THEN 4294967295\n WHEN \'bigint\' THEN 18446744073709551615\n END >> IF(LOCATE(\'unsigned\', COLUMN_TYPE) > 0, 0, 1)\n ) AS auto_increment_ratio\n FROM INFORMATION_SCHEMA.COLUMNS\n INNER JOIN INFORMATION_SCHEMA.TABLES USING (TABLE_SCHEMA, TABLE_NAME)\n WHERE TABLE_SCHEMA NOT IN (\'mysql\', \'sys\', \'INFORMATION_SCHEMA\', \'performance_schema\')\n AND TABLE_TYPE=\'BASE TABLE\'\n AND EXTRA=\'auto_increment\'\n ORDER BY auto_increment_ratio DESC, max_value; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `information_schema`.`columns`.`TABLE_SCHEMA` AS `table_schema`,`information_schema`.`columns`.`TABLE_NAME` AS `table_name`,`information_schema`.`columns`.`COLUMN_NAME` AS `column_name`,`information_schema`.`columns`.`DATA_TYPE` AS `data_type`,`information_schema`.`columns`.`COLUMN_TYPE` AS `column_type`,locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) = 0 AS `is_signed`,locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0 AS `is_unsigned`,case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1) AS `max_value`,`information_schema`.`tables`.`AUTO_INCREMENT` AS `auto_increment`,`information_schema`.`tables`.`AUTO_INCREMENT` / (case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1)) AS `auto_increment_ratio` from (`information_schema`.`columns` join `information_schema`.`tables` on(`information_schema`.`columns`.`TABLE_SCHEMA` = `information_schema`.`tables`.`TABLE_SCHEMA` and `information_schema`.`columns`.`TABLE_NAME` = `information_schema`.`tables`.`TABLE_NAME`)) where `information_schema`.`columns`.`TABLE_SCHEMA` not in (\'mysql\',\'sys\',\'INFORMATION_SCHEMA\',\'performance_schema\') and `information_schema`.`tables`.`TABLE_TYPE` = \'BASE TABLE\' and `information_schema`.`columns`.`EXTRA` = \'auto_increment\' order by `information_schema`.`tables`.`AUTO_INCREMENT` / (case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1)) desc,case `information_schema`.`columns`.`DATA_TYPE` when \'tinyint\' then 255 when \'smallint\' then 65535 when \'mediumint\' then 16777215 when \'int\' then 4294967295 when \'bigint\' then 18446744073709551615 end >> if(locate(\'unsigned\',`information_schema`.`columns`.`COLUMN_TYPE`) > 0,0,1) +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_index_statistics.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_index_statistics.frm new file mode 100644 index 0000000..e7b2660 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_index_statistics.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `table_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `table_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` AS `index_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_FETCH` AS `rows_selected`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_FETCH`) AS `select_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_INSERT` AS `rows_inserted`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT`) AS `insert_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_UPDATE` AS `rows_updated`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_UPDATE`) AS `update_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_DELETE` AS `rows_deleted`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT`) AS `delete_latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is not null order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT` desc +md5=c638e9eebe6bc7efa5fcb523399685de +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365359886 +create-version=2 +source=SELECT OBJECT_SCHEMA AS table_schema,\n OBJECT_NAME AS table_name,\n INDEX_NAME as index_name,\n COUNT_FETCH AS rows_selected,\n format_pico_time(SUM_TIMER_FETCH) AS select_latency,\n COUNT_INSERT AS rows_inserted,\n format_pico_time(SUM_TIMER_INSERT) AS insert_latency,\n COUNT_UPDATE AS rows_updated,\n format_pico_time(SUM_TIMER_UPDATE) AS update_latency,\n COUNT_DELETE AS rows_deleted,\n format_pico_time(SUM_TIMER_INSERT) AS delete_latency\n FROM performance_schema.table_io_waits_summary_by_index_usage\n WHERE index_name IS NOT NULL\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `table_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `table_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` AS `index_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_FETCH` AS `rows_selected`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_FETCH`) AS `select_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_INSERT` AS `rows_inserted`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT`) AS `insert_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_UPDATE` AS `rows_updated`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_UPDATE`) AS `update_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_DELETE` AS `rows_deleted`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT`) AS `delete_latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is not null order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_object_overview.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_object_overview.frm new file mode 100644 index 0000000..cccc95a --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_object_overview.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `information_schema`.`routines`.`ROUTINE_SCHEMA` AS `db`,`information_schema`.`routines`.`ROUTINE_TYPE` AS `object_type`,count(0) AS `count` from `information_schema`.`routines` group by `information_schema`.`routines`.`ROUTINE_SCHEMA`,`information_schema`.`routines`.`ROUTINE_TYPE` union select `information_schema`.`tables`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,`information_schema`.`tables`.`TABLE_TYPE` AS `TABLE_TYPE`,count(0) AS `COUNT(*)` from `information_schema`.`tables` group by `information_schema`.`tables`.`TABLE_SCHEMA`,`information_schema`.`tables`.`TABLE_TYPE` union select `information_schema`.`statistics`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,concat(\'INDEX (\',`information_schema`.`statistics`.`INDEX_TYPE`,\')\') AS `CONCAT(\'INDEX (\', INDEX_TYPE, \')\')`,count(0) AS `COUNT(*)` from `information_schema`.`statistics` group by `information_schema`.`statistics`.`TABLE_SCHEMA`,`information_schema`.`statistics`.`INDEX_TYPE` union select `information_schema`.`triggers`.`TRIGGER_SCHEMA` AS `TRIGGER_SCHEMA`,\'TRIGGER\' AS `TRIGGER`,count(0) AS `COUNT(*)` from `information_schema`.`triggers` group by `information_schema`.`triggers`.`TRIGGER_SCHEMA` union select `information_schema`.`events`.`EVENT_SCHEMA` AS `EVENT_SCHEMA`,\'EVENT\' AS `EVENT`,count(0) AS `COUNT(*)` from `information_schema`.`events` group by `information_schema`.`events`.`EVENT_SCHEMA` order by `db`,`object_type` +md5=be1e4ce9f5bcd017616670d43bbce5ae +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365061127 +create-version=2 +source=SELECT ROUTINE_SCHEMA AS db, ROUTINE_TYPE AS object_type, COUNT(*) AS count FROM information_schema.routines GROUP BY ROUTINE_SCHEMA, ROUTINE_TYPE\n UNION\nSELECT TABLE_SCHEMA, TABLE_TYPE, COUNT(*) FROM information_schema.tables GROUP BY TABLE_SCHEMA, TABLE_TYPE\n UNION\nSELECT TABLE_SCHEMA, CONCAT(\'INDEX (\', INDEX_TYPE, \')\'), COUNT(*) FROM information_schema.statistics GROUP BY TABLE_SCHEMA, INDEX_TYPE\n UNION\nSELECT TRIGGER_SCHEMA, \'TRIGGER\', COUNT(*) FROM information_schema.triggers GROUP BY TRIGGER_SCHEMA\n UNION\nSELECT EVENT_SCHEMA, \'EVENT\', COUNT(*) FROM information_schema.events GROUP BY EVENT_SCHEMA\nORDER BY DB, OBJECT_TYPE; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `information_schema`.`routines`.`ROUTINE_SCHEMA` AS `db`,`information_schema`.`routines`.`ROUTINE_TYPE` AS `object_type`,count(0) AS `count` from `information_schema`.`routines` group by `information_schema`.`routines`.`ROUTINE_SCHEMA`,`information_schema`.`routines`.`ROUTINE_TYPE` union select `information_schema`.`tables`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,`information_schema`.`tables`.`TABLE_TYPE` AS `TABLE_TYPE`,count(0) AS `COUNT(*)` from `information_schema`.`tables` group by `information_schema`.`tables`.`TABLE_SCHEMA`,`information_schema`.`tables`.`TABLE_TYPE` union select `information_schema`.`statistics`.`TABLE_SCHEMA` AS `TABLE_SCHEMA`,concat(\'INDEX (\',`information_schema`.`statistics`.`INDEX_TYPE`,\')\') AS `CONCAT(\'INDEX (\', INDEX_TYPE, \')\')`,count(0) AS `COUNT(*)` from `information_schema`.`statistics` group by `information_schema`.`statistics`.`TABLE_SCHEMA`,`information_schema`.`statistics`.`INDEX_TYPE` union select `information_schema`.`triggers`.`TRIGGER_SCHEMA` AS `TRIGGER_SCHEMA`,\'TRIGGER\' AS `TRIGGER`,count(0) AS `COUNT(*)` from `information_schema`.`triggers` group by `information_schema`.`triggers`.`TRIGGER_SCHEMA` union select `information_schema`.`events`.`EVENT_SCHEMA` AS `EVENT_SCHEMA`,\'EVENT\' AS `EVENT`,count(0) AS `COUNT(*)` from `information_schema`.`events` group by `information_schema`.`events`.`EVENT_SCHEMA` order by `db`,`object_type` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_redundant_indexes.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_redundant_indexes.frm new file mode 100644 index 0000000..39c230d --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_redundant_indexes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `redundant_keys`.`table_schema` AS `table_schema`,`redundant_keys`.`table_name` AS `table_name`,`redundant_keys`.`index_name` AS `redundant_index_name`,`redundant_keys`.`index_columns` AS `redundant_index_columns`,`redundant_keys`.`non_unique` AS `redundant_index_non_unique`,`dominant_keys`.`index_name` AS `dominant_index_name`,`dominant_keys`.`index_columns` AS `dominant_index_columns`,`dominant_keys`.`non_unique` AS `dominant_index_non_unique`,if(`redundant_keys`.`subpart_exists` <> 0 or `dominant_keys`.`subpart_exists` <> 0,1,0) AS `subpart_exists`,concat(\'ALTER TABLE `\',`redundant_keys`.`table_schema`,\'`.`\',`redundant_keys`.`table_name`,\'` DROP INDEX `\',`redundant_keys`.`index_name`,\'`\') AS `sql_drop_index` from (`sys`.`x$schema_flattened_keys` `redundant_keys` join `sys`.`x$schema_flattened_keys` `dominant_keys` on(`redundant_keys`.`table_schema` = `dominant_keys`.`table_schema` and `redundant_keys`.`table_name` = `dominant_keys`.`table_name`)) where `redundant_keys`.`index_name` <> `dominant_keys`.`index_name` and (`redundant_keys`.`index_columns` = `dominant_keys`.`index_columns` and (`redundant_keys`.`non_unique` > `dominant_keys`.`non_unique` or `redundant_keys`.`non_unique` = `dominant_keys`.`non_unique` and if(`redundant_keys`.`index_name` = \'PRIMARY\',\'\',`redundant_keys`.`index_name`) > if(`dominant_keys`.`index_name` = \'PRIMARY\',\'\',`dominant_keys`.`index_name`)) or locate(concat(`redundant_keys`.`index_columns`,\',\'),`dominant_keys`.`index_columns`) = 1 and `redundant_keys`.`non_unique` = 1 or locate(concat(`dominant_keys`.`index_columns`,\',\'),`redundant_keys`.`index_columns`) = 1 and `dominant_keys`.`non_unique` = 0) +md5=b7dc42e5df448cf4a08d3059e8ecf40f +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365103193 +create-version=2 +source=SELECT\n redundant_keys.table_schema,\n redundant_keys.table_name,\n redundant_keys.index_name AS redundant_index_name,\n redundant_keys.index_columns AS redundant_index_columns,\n redundant_keys.non_unique AS redundant_index_non_unique,\n dominant_keys.index_name AS dominant_index_name,\n dominant_keys.index_columns AS dominant_index_columns,\n dominant_keys.non_unique AS dominant_index_non_unique,\n IF(redundant_keys.subpart_exists OR dominant_keys.subpart_exists, 1 ,0) AS subpart_exists,\n CONCAT(\n \'ALTER TABLE `\', redundant_keys.table_schema, \'`.`\', redundant_keys.table_name, \'` DROP INDEX `\', redundant_keys.index_name, \'`\'\n ) AS sql_drop_index\n FROM\n x$schema_flattened_keys AS redundant_keys\n INNER JOIN x$schema_flattened_keys AS dominant_keys\n USING (TABLE_SCHEMA, TABLE_NAME)\n WHERE\n redundant_keys.index_name != dominant_keys.index_name\n AND (\n (\n /* Identical columns */\n (redundant_keys.index_columns = dominant_keys.index_columns)\n AND (\n (redundant_keys.non_unique > dominant_keys.non_unique)\n OR (redundant_keys.non_unique = dominant_keys.non_unique\n AND IF(redundant_keys.index_name=\'PRIMARY\', \'\', redundant_keys.index_name) > IF(dominant_keys.index_name=\'PRIMARY\', \'\', dominant_keys.index_name)\n )\n )\n )\n OR\n (\n /* Non-unique prefix columns */\n LOCATE(CONCAT(redundant_keys.index_columns, \',\'), dominant_keys.index_columns) = 1\n AND redundant_keys.non_unique = 1\n )\n OR\n (\n /* Unique prefix columns */\n LOCATE(CONCAT(dominant_keys.index_columns, \',\'), redundant_keys.index_columns) = 1\n AND dominant_keys.non_unique = 0\n )\n ); +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `redundant_keys`.`table_schema` AS `table_schema`,`redundant_keys`.`table_name` AS `table_name`,`redundant_keys`.`index_name` AS `redundant_index_name`,`redundant_keys`.`index_columns` AS `redundant_index_columns`,`redundant_keys`.`non_unique` AS `redundant_index_non_unique`,`dominant_keys`.`index_name` AS `dominant_index_name`,`dominant_keys`.`index_columns` AS `dominant_index_columns`,`dominant_keys`.`non_unique` AS `dominant_index_non_unique`,if(`redundant_keys`.`subpart_exists` <> 0 or `dominant_keys`.`subpart_exists` <> 0,1,0) AS `subpart_exists`,concat(\'ALTER TABLE `\',`redundant_keys`.`table_schema`,\'`.`\',`redundant_keys`.`table_name`,\'` DROP INDEX `\',`redundant_keys`.`index_name`,\'`\') AS `sql_drop_index` from (`sys`.`x$schema_flattened_keys` `redundant_keys` join `sys`.`x$schema_flattened_keys` `dominant_keys` on(`redundant_keys`.`table_schema` = `dominant_keys`.`table_schema` and `redundant_keys`.`table_name` = `dominant_keys`.`table_name`)) where `redundant_keys`.`index_name` <> `dominant_keys`.`index_name` and (`redundant_keys`.`index_columns` = `dominant_keys`.`index_columns` and (`redundant_keys`.`non_unique` > `dominant_keys`.`non_unique` or `redundant_keys`.`non_unique` = `dominant_keys`.`non_unique` and if(`redundant_keys`.`index_name` = \'PRIMARY\',\'\',`redundant_keys`.`index_name`) > if(`dominant_keys`.`index_name` = \'PRIMARY\',\'\',`dominant_keys`.`index_name`)) or locate(concat(`redundant_keys`.`index_columns`,\',\'),`dominant_keys`.`index_columns`) = 1 and `redundant_keys`.`non_unique` = 1 or locate(concat(`dominant_keys`.`index_columns`,\',\'),`redundant_keys`.`index_columns`) = 1 and `dominant_keys`.`non_unique` = 0) +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_table_lock_waits.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_table_lock_waits.frm new file mode 100644 index 0000000..a356a43 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_table_lock_waits.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `g`.`OBJECT_SCHEMA` AS `object_schema`,`g`.`OBJECT_NAME` AS `object_name`,`pt`.`THREAD_ID` AS `waiting_thread_id`,`pt`.`PROCESSLIST_ID` AS `waiting_pid`,`sys`.`ps_thread_account`(`p`.`OWNER_THREAD_ID`) AS `waiting_account`,`p`.`LOCK_TYPE` AS `waiting_lock_type`,`p`.`LOCK_DURATION` AS `waiting_lock_duration`,`sys`.`format_statement`(`pt`.`PROCESSLIST_INFO`) AS `waiting_query`,`pt`.`PROCESSLIST_TIME` AS `waiting_query_secs`,`ps`.`ROWS_AFFECTED` AS `waiting_query_rows_affected`,`ps`.`ROWS_EXAMINED` AS `waiting_query_rows_examined`,`gt`.`THREAD_ID` AS `blocking_thread_id`,`gt`.`PROCESSLIST_ID` AS `blocking_pid`,`sys`.`ps_thread_account`(`g`.`OWNER_THREAD_ID`) AS `blocking_account`,`g`.`LOCK_TYPE` AS `blocking_lock_type`,`g`.`LOCK_DURATION` AS `blocking_lock_duration`,concat(\'KILL QUERY \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_query`,concat(\'KILL \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_connection` from (((((`performance_schema`.`metadata_locks` `g` join `performance_schema`.`metadata_locks` `p` on(`g`.`OBJECT_TYPE` = `p`.`OBJECT_TYPE` and `g`.`OBJECT_SCHEMA` = `p`.`OBJECT_SCHEMA` and `g`.`OBJECT_NAME` = `p`.`OBJECT_NAME` and `g`.`LOCK_STATUS` = \'GRANTED\' and `p`.`LOCK_STATUS` = \'PENDING\')) join `performance_schema`.`threads` `gt` on(`g`.`OWNER_THREAD_ID` = `gt`.`THREAD_ID`)) join `performance_schema`.`threads` `pt` on(`p`.`OWNER_THREAD_ID` = `pt`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `gs` on(`g`.`OWNER_THREAD_ID` = `gs`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `ps` on(`p`.`OWNER_THREAD_ID` = `ps`.`THREAD_ID`)) where `g`.`OBJECT_TYPE` = \'TABLE\' +md5=0e529ab6c702966e113a44dea76f5a90 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365472707 +create-version=2 +source=SELECT g.object_schema AS object_schema,\n g.object_name AS object_name,\n pt.thread_id AS waiting_thread_id,\n pt.processlist_id AS waiting_pid,\n sys.ps_thread_account(p.owner_thread_id) AS waiting_account,\n p.lock_type AS waiting_lock_type,\n p.lock_duration AS waiting_lock_duration,\n sys.format_statement(pt.processlist_info) AS waiting_query,\n pt.processlist_time AS waiting_query_secs,\n ps.rows_affected AS waiting_query_rows_affected,\n ps.rows_examined AS waiting_query_rows_examined,\n gt.thread_id AS blocking_thread_id,\n gt.processlist_id AS blocking_pid,\n sys.ps_thread_account(g.owner_thread_id) AS blocking_account,\n g.lock_type AS blocking_lock_type,\n g.lock_duration AS blocking_lock_duration,\n CONCAT(\'KILL QUERY \', gt.processlist_id) AS sql_kill_blocking_query,\n CONCAT(\'KILL \', gt.processlist_id) AS sql_kill_blocking_connection\n FROM performance_schema.metadata_locks g\n INNER JOIN performance_schema.metadata_locks p\n ON g.object_type = p.object_type\n AND g.object_schema = p.object_schema\n AND g.object_name = p.object_name\n AND g.lock_status = \'GRANTED\'\n AND p.lock_status = \'PENDING\'\n INNER JOIN performance_schema.threads gt ON g.owner_thread_id = gt.thread_id\n INNER JOIN performance_schema.threads pt ON p.owner_thread_id = pt.thread_id\n LEFT JOIN performance_schema.events_statements_current gs ON g.owner_thread_id = gs.thread_id\n LEFT JOIN performance_schema.events_statements_current ps ON p.owner_thread_id = ps.thread_id\n WHERE g.object_type = \'TABLE\'; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `g`.`OBJECT_SCHEMA` AS `object_schema`,`g`.`OBJECT_NAME` AS `object_name`,`pt`.`THREAD_ID` AS `waiting_thread_id`,`pt`.`PROCESSLIST_ID` AS `waiting_pid`,`sys`.`ps_thread_account`(`p`.`OWNER_THREAD_ID`) AS `waiting_account`,`p`.`LOCK_TYPE` AS `waiting_lock_type`,`p`.`LOCK_DURATION` AS `waiting_lock_duration`,`sys`.`format_statement`(`pt`.`PROCESSLIST_INFO`) AS `waiting_query`,`pt`.`PROCESSLIST_TIME` AS `waiting_query_secs`,`ps`.`ROWS_AFFECTED` AS `waiting_query_rows_affected`,`ps`.`ROWS_EXAMINED` AS `waiting_query_rows_examined`,`gt`.`THREAD_ID` AS `blocking_thread_id`,`gt`.`PROCESSLIST_ID` AS `blocking_pid`,`sys`.`ps_thread_account`(`g`.`OWNER_THREAD_ID`) AS `blocking_account`,`g`.`LOCK_TYPE` AS `blocking_lock_type`,`g`.`LOCK_DURATION` AS `blocking_lock_duration`,concat(\'KILL QUERY \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_query`,concat(\'KILL \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_connection` from (((((`performance_schema`.`metadata_locks` `g` join `performance_schema`.`metadata_locks` `p` on(`g`.`OBJECT_TYPE` = `p`.`OBJECT_TYPE` and `g`.`OBJECT_SCHEMA` = `p`.`OBJECT_SCHEMA` and `g`.`OBJECT_NAME` = `p`.`OBJECT_NAME` and `g`.`LOCK_STATUS` = \'GRANTED\' and `p`.`LOCK_STATUS` = \'PENDING\')) join `performance_schema`.`threads` `gt` on(`g`.`OWNER_THREAD_ID` = `gt`.`THREAD_ID`)) join `performance_schema`.`threads` `pt` on(`p`.`OWNER_THREAD_ID` = `pt`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `gs` on(`g`.`OWNER_THREAD_ID` = `gs`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `ps` on(`p`.`OWNER_THREAD_ID` = `ps`.`THREAD_ID`)) where `g`.`OBJECT_TYPE` = \'TABLE\' +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_table_statistics.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_table_statistics.frm new file mode 100644 index 0000000..28ae46d --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_table_statistics.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,format_pico_time(`pst`.`SUM_TIMER_WAIT`) AS `total_latency`,`pst`.`COUNT_FETCH` AS `rows_fetched`,format_pico_time(`pst`.`SUM_TIMER_FETCH`) AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,format_pico_time(`pst`.`SUM_TIMER_INSERT`) AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,format_pico_time(`pst`.`SUM_TIMER_UPDATE`) AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,format_pico_time(`pst`.`SUM_TIMER_DELETE`) AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_read`) AS `io_read`,format_pico_time(`fsbi`.`sum_timer_read`) AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_write`) AS `io_write`,format_pico_time(`fsbi`.`sum_timer_write`) AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,format_pico_time(`fsbi`.`sum_timer_misc`) AS `io_misc_latency` from (`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +md5=74a7b27f66a13611050e4ea8186510b4 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365396204 +create-version=2 +source=SELECT pst.object_schema AS table_schema,\n pst.object_name AS table_name,\n format_pico_time(pst.sum_timer_wait) AS total_latency,\n pst.count_fetch AS rows_fetched,\n format_pico_time(pst.sum_timer_fetch) AS fetch_latency,\n pst.count_insert AS rows_inserted,\n format_pico_time(pst.sum_timer_insert) AS insert_latency,\n pst.count_update AS rows_updated,\n format_pico_time(pst.sum_timer_update) AS update_latency,\n pst.count_delete AS rows_deleted,\n format_pico_time(pst.sum_timer_delete) AS delete_latency,\n fsbi.count_read AS io_read_requests,\n sys.format_bytes(fsbi.sum_number_of_bytes_read) AS io_read,\n format_pico_time(fsbi.sum_timer_read) AS io_read_latency,\n fsbi.count_write AS io_write_requests,\n sys.format_bytes(fsbi.sum_number_of_bytes_write) AS io_write,\n format_pico_time(fsbi.sum_timer_write) AS io_write_latency,\n fsbi.count_misc AS io_misc_requests,\n format_pico_time(fsbi.sum_timer_misc) AS io_misc_latency\n FROM performance_schema.table_io_waits_summary_by_table AS pst\n LEFT JOIN x$ps_schema_table_statistics_io AS fsbi\n ON pst.object_schema = fsbi.table_schema\n AND pst.object_name = fsbi.table_name\n ORDER BY pst.sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,format_pico_time(`pst`.`SUM_TIMER_WAIT`) AS `total_latency`,`pst`.`COUNT_FETCH` AS `rows_fetched`,format_pico_time(`pst`.`SUM_TIMER_FETCH`) AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,format_pico_time(`pst`.`SUM_TIMER_INSERT`) AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,format_pico_time(`pst`.`SUM_TIMER_UPDATE`) AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,format_pico_time(`pst`.`SUM_TIMER_DELETE`) AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_read`) AS `io_read`,format_pico_time(`fsbi`.`sum_timer_read`) AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_write`) AS `io_write`,format_pico_time(`fsbi`.`sum_timer_write`) AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,format_pico_time(`fsbi`.`sum_timer_misc`) AS `io_misc_latency` from (`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_table_statistics_with_buffer.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_table_statistics_with_buffer.frm new file mode 100644 index 0000000..337073f --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_table_statistics_with_buffer.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,`pst`.`COUNT_FETCH` AS `rows_fetched`,format_pico_time(`pst`.`SUM_TIMER_FETCH`) AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,format_pico_time(`pst`.`SUM_TIMER_INSERT`) AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,format_pico_time(`pst`.`SUM_TIMER_UPDATE`) AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,format_pico_time(`pst`.`SUM_TIMER_DELETE`) AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_read`) AS `io_read`,format_pico_time(`fsbi`.`sum_timer_read`) AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_write`) AS `io_write`,format_pico_time(`fsbi`.`sum_timer_write`) AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,format_pico_time(`fsbi`.`sum_timer_misc`) AS `io_misc_latency`,`sys`.`format_bytes`(`ibp`.`allocated`) AS `innodb_buffer_allocated`,`sys`.`format_bytes`(`ibp`.`data`) AS `innodb_buffer_data`,`sys`.`format_bytes`(`ibp`.`allocated` - `ibp`.`data`) AS `innodb_buffer_free`,`ibp`.`pages` AS `innodb_buffer_pages`,`ibp`.`pages_hashed` AS `innodb_buffer_pages_hashed`,`ibp`.`pages_old` AS `innodb_buffer_pages_old`,`ibp`.`rows_cached` AS `innodb_buffer_rows_cached` from ((`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) left join `sys`.`x$innodb_buffer_stats_by_table` `ibp` on(`pst`.`OBJECT_SCHEMA` = `ibp`.`object_schema` and `pst`.`OBJECT_NAME` = `ibp`.`object_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +md5=babe17622733f781733c5d9063af3aaf +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365418745 +create-version=2 +source=SELECT pst.object_schema AS table_schema,\n pst.object_name AS table_name,\n pst.count_fetch AS rows_fetched,\n format_pico_time(pst.sum_timer_fetch) AS fetch_latency,\n pst.count_insert AS rows_inserted,\n format_pico_time(pst.sum_timer_insert) AS insert_latency,\n pst.count_update AS rows_updated,\n format_pico_time(pst.sum_timer_update) AS update_latency,\n pst.count_delete AS rows_deleted,\n format_pico_time(pst.sum_timer_delete) AS delete_latency,\n fsbi.count_read AS io_read_requests,\n sys.format_bytes(fsbi.sum_number_of_bytes_read) AS io_read,\n format_pico_time(fsbi.sum_timer_read) AS io_read_latency,\n fsbi.count_write AS io_write_requests,\n sys.format_bytes(fsbi.sum_number_of_bytes_write) AS io_write,\n format_pico_time(fsbi.sum_timer_write) AS io_write_latency,\n fsbi.count_misc AS io_misc_requests,\n format_pico_time(fsbi.sum_timer_misc) AS io_misc_latency,\n sys.format_bytes(ibp.allocated) AS innodb_buffer_allocated,\n sys.format_bytes(ibp.data) AS innodb_buffer_data,\n sys.format_bytes(ibp.allocated - ibp.data) AS innodb_buffer_free,\n ibp.pages AS innodb_buffer_pages,\n ibp.pages_hashed AS innodb_buffer_pages_hashed,\n ibp.pages_old AS innodb_buffer_pages_old,\n ibp.rows_cached AS innodb_buffer_rows_cached\n FROM performance_schema.table_io_waits_summary_by_table AS pst\n LEFT JOIN x$ps_schema_table_statistics_io AS fsbi\n ON pst.object_schema = fsbi.table_schema\n AND pst.object_name = fsbi.table_name\n LEFT JOIN sys.x$innodb_buffer_stats_by_table AS ibp\n ON pst.object_schema = ibp.object_schema\n AND pst.object_name = ibp.object_name\n ORDER BY pst.sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,`pst`.`COUNT_FETCH` AS `rows_fetched`,format_pico_time(`pst`.`SUM_TIMER_FETCH`) AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,format_pico_time(`pst`.`SUM_TIMER_INSERT`) AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,format_pico_time(`pst`.`SUM_TIMER_UPDATE`) AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,format_pico_time(`pst`.`SUM_TIMER_DELETE`) AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_read`) AS `io_read`,format_pico_time(`fsbi`.`sum_timer_read`) AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`sys`.`format_bytes`(`fsbi`.`sum_number_of_bytes_write`) AS `io_write`,format_pico_time(`fsbi`.`sum_timer_write`) AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,format_pico_time(`fsbi`.`sum_timer_misc`) AS `io_misc_latency`,`sys`.`format_bytes`(`ibp`.`allocated`) AS `innodb_buffer_allocated`,`sys`.`format_bytes`(`ibp`.`data`) AS `innodb_buffer_data`,`sys`.`format_bytes`(`ibp`.`allocated` - `ibp`.`data`) AS `innodb_buffer_free`,`ibp`.`pages` AS `innodb_buffer_pages`,`ibp`.`pages_hashed` AS `innodb_buffer_pages_hashed`,`ibp`.`pages_old` AS `innodb_buffer_pages_old`,`ibp`.`rows_cached` AS `innodb_buffer_rows_cached` from ((`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) left join `sys`.`x$innodb_buffer_stats_by_table` `ibp` on(`pst`.`OBJECT_SCHEMA` = `ibp`.`object_schema` and `pst`.`OBJECT_NAME` = `ibp`.`object_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_tables_with_full_table_scans.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_tables_with_full_table_scans.frm new file mode 100644 index 0000000..ccd43cc --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_tables_with_full_table_scans.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `object_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `object_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` AS `rows_full_scanned`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT`) AS `latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is null and `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` > 0 order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` desc +md5=dcd97456a8df9123b94972666be066a5 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365437344 +create-version=2 +source=SELECT object_schema,\n object_name,\n count_read AS rows_full_scanned,\n format_pico_time(sum_timer_wait) AS latency\n FROM performance_schema.table_io_waits_summary_by_index_usage\n WHERE index_name IS NULL\n AND count_read > 0\n ORDER BY count_read DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `object_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `object_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` AS `rows_full_scanned`,format_pico_time(`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT`) AS `latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is null and `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` > 0 order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/schema_unused_indexes.frm b/src/main/docker-mariadb/mariadb_data/sys/schema_unused_indexes.frm new file mode 100644 index 0000000..0b6a658 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/schema_unused_indexes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `object_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `object_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` AS `index_name` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is not null and `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_STAR` = 0 and `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` <> \'mysql\' and `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` <> \'PRIMARY\' order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` +md5=fcec883d9422ee0089c8d60c48eb8238 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365456772 +create-version=2 +source=SELECT object_schema,\n object_name,\n index_name\n FROM performance_schema.table_io_waits_summary_by_index_usage\n WHERE index_name IS NOT NULL\n AND count_star = 0\n AND object_schema != \'mysql\'\n AND index_name != \'PRIMARY\'\n ORDER BY object_schema, object_name; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `object_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `object_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` AS `index_name` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is not null and `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_STAR` = 0 and `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` <> \'mysql\' and `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` <> \'PRIMARY\' order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/session.frm b/src/main/docker-mariadb/mariadb_data/sys/session.frm new file mode 100644 index 0000000..434d23e --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/session.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `processlist`.`thd_id` AS `thd_id`,`processlist`.`conn_id` AS `conn_id`,`processlist`.`user` AS `user`,`processlist`.`db` AS `db`,`processlist`.`command` AS `command`,`processlist`.`state` AS `state`,`processlist`.`time` AS `time`,`processlist`.`current_statement` AS `current_statement`,`processlist`.`statement_latency` AS `statement_latency`,`processlist`.`progress` AS `progress`,`processlist`.`lock_latency` AS `lock_latency`,`processlist`.`rows_examined` AS `rows_examined`,`processlist`.`rows_sent` AS `rows_sent`,`processlist`.`rows_affected` AS `rows_affected`,`processlist`.`tmp_tables` AS `tmp_tables`,`processlist`.`tmp_disk_tables` AS `tmp_disk_tables`,`processlist`.`full_scan` AS `full_scan`,`processlist`.`last_statement` AS `last_statement`,`processlist`.`last_statement_latency` AS `last_statement_latency`,`processlist`.`current_memory` AS `current_memory`,`processlist`.`last_wait` AS `last_wait`,`processlist`.`last_wait_latency` AS `last_wait_latency`,`processlist`.`source` AS `source`,`processlist`.`trx_latency` AS `trx_latency`,`processlist`.`trx_state` AS `trx_state`,`processlist`.`trx_autocommit` AS `trx_autocommit`,`processlist`.`pid` AS `pid`,`processlist`.`program_name` AS `program_name` from `sys`.`processlist` where `processlist`.`conn_id` is not null and `processlist`.`command` <> \'Daemon\' +md5=5d945dd589c16b1108e948bdcabafcf3 +updatable=0 +algorithm=0 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298366033861 +create-version=2 +source=SELECT * FROM sys.processlist\nWHERE conn_id IS NOT NULL AND command != \'Daemon\'; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `processlist`.`thd_id` AS `thd_id`,`processlist`.`conn_id` AS `conn_id`,`processlist`.`user` AS `user`,`processlist`.`db` AS `db`,`processlist`.`command` AS `command`,`processlist`.`state` AS `state`,`processlist`.`time` AS `time`,`processlist`.`current_statement` AS `current_statement`,`processlist`.`statement_latency` AS `statement_latency`,`processlist`.`progress` AS `progress`,`processlist`.`lock_latency` AS `lock_latency`,`processlist`.`rows_examined` AS `rows_examined`,`processlist`.`rows_sent` AS `rows_sent`,`processlist`.`rows_affected` AS `rows_affected`,`processlist`.`tmp_tables` AS `tmp_tables`,`processlist`.`tmp_disk_tables` AS `tmp_disk_tables`,`processlist`.`full_scan` AS `full_scan`,`processlist`.`last_statement` AS `last_statement`,`processlist`.`last_statement_latency` AS `last_statement_latency`,`processlist`.`current_memory` AS `current_memory`,`processlist`.`last_wait` AS `last_wait`,`processlist`.`last_wait_latency` AS `last_wait_latency`,`processlist`.`source` AS `source`,`processlist`.`trx_latency` AS `trx_latency`,`processlist`.`trx_state` AS `trx_state`,`processlist`.`trx_autocommit` AS `trx_autocommit`,`processlist`.`pid` AS `pid`,`processlist`.`program_name` AS `program_name` from `sys`.`processlist` where `processlist`.`conn_id` is not null and `processlist`.`command` <> \'Daemon\' +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/session_ssl_status.frm b/src/main/docker-mariadb/mariadb_data/sys/session_ssl_status.frm new file mode 100644 index 0000000..affcb9d --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/session_ssl_status.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sslver`.`THREAD_ID` AS `thread_id`,`sslver`.`VARIABLE_VALUE` AS `ssl_version`,`sslcip`.`VARIABLE_VALUE` AS `ssl_cipher`,`sslreuse`.`VARIABLE_VALUE` AS `ssl_sessions_reused` from ((`performance_schema`.`status_by_thread` `sslver` left join `performance_schema`.`status_by_thread` `sslcip` on(`sslcip`.`THREAD_ID` = `sslver`.`THREAD_ID` and `sslcip`.`VARIABLE_NAME` = \'Ssl_cipher\')) left join `performance_schema`.`status_by_thread` `sslreuse` on(`sslreuse`.`THREAD_ID` = `sslver`.`THREAD_ID` and `sslreuse`.`VARIABLE_NAME` = \'Ssl_sessions_reused\')) where `sslver`.`VARIABLE_NAME` = \'Ssl_version\' +md5=888bde4bd747f7df3ec788d97818af55 +updatable=0 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298366058300 +create-version=2 +source=SELECT sslver.thread_id,\n sslver.variable_value ssl_version,\n sslcip.variable_value ssl_cipher,\n sslreuse.variable_value ssl_sessions_reused\n FROM performance_schema.status_by_thread sslver\n LEFT JOIN performance_schema.status_by_thread sslcip\n ON (sslcip.thread_id=sslver.thread_id and sslcip.variable_name=\'Ssl_cipher\')\n LEFT JOIN performance_schema.status_by_thread sslreuse\n ON (sslreuse.thread_id=sslver.thread_id and sslreuse.variable_name=\'Ssl_sessions_reused\')\n WHERE sslver.variable_name=\'Ssl_version\'; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sslver`.`THREAD_ID` AS `thread_id`,`sslver`.`VARIABLE_VALUE` AS `ssl_version`,`sslcip`.`VARIABLE_VALUE` AS `ssl_cipher`,`sslreuse`.`VARIABLE_VALUE` AS `ssl_sessions_reused` from ((`performance_schema`.`status_by_thread` `sslver` left join `performance_schema`.`status_by_thread` `sslcip` on(`sslcip`.`THREAD_ID` = `sslver`.`THREAD_ID` and `sslcip`.`VARIABLE_NAME` = \'Ssl_cipher\')) left join `performance_schema`.`status_by_thread` `sslreuse` on(`sslreuse`.`THREAD_ID` = `sslver`.`THREAD_ID` and `sslreuse`.`VARIABLE_NAME` = \'Ssl_sessions_reused\')) where `sslver`.`VARIABLE_NAME` = \'Ssl_version\' +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/statement_analysis.frm b/src/main/docker-mariadb/mariadb_data/sys/statement_analysis.frm new file mode 100644 index 0000000..7f99e35 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/statement_analysis.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,if(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `err_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warn_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_LOCK_TIME`) AS `lock_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` AS `rows_affected`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_affected_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen` from `performance_schema`.`events_statements_summary_by_digest` order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +md5=e037e136516fae531ffa6ce3d9fc603f +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365492366 +create-version=2 +source=SELECT sys.format_statement(DIGEST_TEXT) AS query,\n SCHEMA_NAME AS db,\n IF(SUM_NO_GOOD_INDEX_USED > 0 OR SUM_NO_INDEX_USED > 0, \'*\', \'\') AS full_scan,\n COUNT_STAR AS exec_count,\n SUM_ERRORS AS err_count,\n SUM_WARNINGS AS warn_count,\n format_pico_time(SUM_TIMER_WAIT) AS total_latency,\n format_pico_time(MAX_TIMER_WAIT) AS max_latency,\n format_pico_time(AVG_TIMER_WAIT) AS avg_latency,\n format_pico_time(SUM_LOCK_TIME) AS lock_latency,\n SUM_ROWS_SENT AS rows_sent,\n ROUND(IFNULL(SUM_ROWS_SENT / NULLIF(COUNT_STAR, 0), 0)) AS rows_sent_avg,\n SUM_ROWS_EXAMINED AS rows_examined,\n ROUND(IFNULL(SUM_ROWS_EXAMINED / NULLIF(COUNT_STAR, 0), 0)) AS rows_examined_avg,\n SUM_ROWS_AFFECTED AS rows_affected,\n ROUND(IFNULL(SUM_ROWS_AFFECTED / NULLIF(COUNT_STAR, 0), 0)) AS rows_affected_avg,\n SUM_CREATED_TMP_TABLES AS tmp_tables,\n SUM_CREATED_TMP_DISK_TABLES AS tmp_disk_tables,\n SUM_SORT_ROWS AS rows_sorted,\n SUM_SORT_MERGE_PASSES AS sort_merge_passes,\n DIGEST AS digest,\n FIRST_SEEN AS first_seen,\n LAST_SEEN as last_seen\n FROM performance_schema.events_statements_summary_by_digest\nORDER BY SUM_TIMER_WAIT DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,if(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `err_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warn_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_LOCK_TIME`) AS `lock_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` AS `rows_affected`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_affected_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen` from `performance_schema`.`events_statements_summary_by_digest` order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/statements_with_errors_or_warnings.frm b/src/main/docker-mariadb/mariadb_data/sys/statements_with_errors_or_warnings.frm new file mode 100644 index 0000000..e98961e --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/statements_with_errors_or_warnings.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `errors`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `error_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warnings`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `warning_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` desc +md5=0d5bc7a3a6b50253b6b71e214beb1104 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365510464 +create-version=2 +source=SELECT sys.format_statement(DIGEST_TEXT) AS query,\n SCHEMA_NAME as db,\n COUNT_STAR AS exec_count,\n SUM_ERRORS AS errors,\n IFNULL(SUM_ERRORS / NULLIF(COUNT_STAR, 0), 0) * 100 as error_pct,\n SUM_WARNINGS AS warnings,\n IFNULL(SUM_WARNINGS / NULLIF(COUNT_STAR, 0), 0) * 100 as warning_pct,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE SUM_ERRORS > 0\n OR SUM_WARNINGS > 0\nORDER BY SUM_ERRORS DESC, SUM_WARNINGS DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `errors`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `error_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warnings`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `warning_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/statements_with_full_table_scans.frm b/src/main/docker-mariadb/mariadb_data/sys/statements_with_full_table_scans.frm new file mode 100644 index 0000000..cef73a5 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/statements_with_full_table_scans.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` AS `no_index_used_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` AS `no_good_index_used_count`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) AS `no_index_used_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_sent_avg`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where (`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0) and `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` not like \'SHOW%\' order by round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) desc,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) desc +md5=c15d5fb47b5ab4f0a5b4d8c50379a100 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365530061 +create-version=2 +source=SELECT sys.format_statement(DIGEST_TEXT) AS query,\n SCHEMA_NAME as db,\n COUNT_STAR AS exec_count,\n format_pico_time(SUM_TIMER_WAIT) AS total_latency,\n SUM_NO_INDEX_USED AS no_index_used_count,\n SUM_NO_GOOD_INDEX_USED AS no_good_index_used_count,\n ROUND(IFNULL(SUM_NO_INDEX_USED / NULLIF(COUNT_STAR, 0), 0) * 100) AS no_index_used_pct,\n SUM_ROWS_SENT AS rows_sent,\n SUM_ROWS_EXAMINED AS rows_examined,\n ROUND(SUM_ROWS_SENT/COUNT_STAR) AS rows_sent_avg,\n ROUND(SUM_ROWS_EXAMINED/COUNT_STAR) AS rows_examined_avg,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE (SUM_NO_INDEX_USED > 0\n OR SUM_NO_GOOD_INDEX_USED > 0)\n AND DIGEST_TEXT NOT LIKE \'SHOW%\'\n ORDER BY no_index_used_pct DESC, total_latency DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` AS `no_index_used_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` AS `no_good_index_used_count`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) AS `no_index_used_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_sent_avg`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where (`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0) and `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` not like \'SHOW%\' order by round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) desc,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/statements_with_runtimes_in_95th_percentile.frm b/src/main/docker-mariadb/mariadb_data/sys/statements_with_runtimes_in_95th_percentile.frm new file mode 100644 index 0000000..c716be6 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/statements_with_runtimes_in_95th_percentile.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_statement`(`stmts`.`DIGEST_TEXT`) AS `query`,`stmts`.`SCHEMA_NAME` AS `db`,if(`stmts`.`SUM_NO_GOOD_INDEX_USED` > 0 or `stmts`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`stmts`.`COUNT_STAR` AS `exec_count`,`stmts`.`SUM_ERRORS` AS `err_count`,`stmts`.`SUM_WARNINGS` AS `warn_count`,format_pico_time(`stmts`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`stmts`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`stmts`.`AVG_TIMER_WAIT`) AS `avg_latency`,`stmts`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`stmts`.`SUM_ROWS_SENT` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`stmts`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`stmts`.`SUM_ROWS_EXAMINED` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`stmts`.`FIRST_SEEN` AS `first_seen`,`stmts`.`LAST_SEEN` AS `last_seen`,`stmts`.`DIGEST` AS `digest` from (`performance_schema`.`events_statements_summary_by_digest` `stmts` join `sys`.`x$ps_digest_95th_percentile_by_avg_us` `top_percentile` on(round(`stmts`.`AVG_TIMER_WAIT` / 1000000,0) >= `top_percentile`.`avg_us`)) order by `stmts`.`AVG_TIMER_WAIT` desc +md5=d6f2238ee4994e7c6edfd0d09d4d5e1d +updatable=0 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365587403 +create-version=2 +source=SELECT sys.format_statement(DIGEST_TEXT) AS query,\n SCHEMA_NAME as db,\n IF(SUM_NO_GOOD_INDEX_USED > 0 OR SUM_NO_INDEX_USED > 0, \'*\', \'\') AS full_scan,\n COUNT_STAR AS exec_count,\n SUM_ERRORS AS err_count,\n SUM_WARNINGS AS warn_count,\n format_pico_time(SUM_TIMER_WAIT) AS total_latency,\n format_pico_time(MAX_TIMER_WAIT) AS max_latency,\n format_pico_time(AVG_TIMER_WAIT) AS avg_latency,\n SUM_ROWS_SENT AS rows_sent,\n ROUND(IFNULL(SUM_ROWS_SENT / NULLIF(COUNT_STAR, 0), 0)) AS rows_sent_avg,\n SUM_ROWS_EXAMINED AS rows_examined,\n ROUND(IFNULL(SUM_ROWS_EXAMINED / NULLIF(COUNT_STAR, 0), 0)) AS rows_examined_avg,\n FIRST_SEEN AS first_seen,\n LAST_SEEN AS last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest stmts\n JOIN sys.x$ps_digest_95th_percentile_by_avg_us AS top_percentile\n ON ROUND(stmts.avg_timer_wait/1000000) >= top_percentile.avg_us\n ORDER BY AVG_TIMER_WAIT DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_statement`(`stmts`.`DIGEST_TEXT`) AS `query`,`stmts`.`SCHEMA_NAME` AS `db`,if(`stmts`.`SUM_NO_GOOD_INDEX_USED` > 0 or `stmts`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`stmts`.`COUNT_STAR` AS `exec_count`,`stmts`.`SUM_ERRORS` AS `err_count`,`stmts`.`SUM_WARNINGS` AS `warn_count`,format_pico_time(`stmts`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`stmts`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`stmts`.`AVG_TIMER_WAIT`) AS `avg_latency`,`stmts`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`stmts`.`SUM_ROWS_SENT` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`stmts`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`stmts`.`SUM_ROWS_EXAMINED` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`stmts`.`FIRST_SEEN` AS `first_seen`,`stmts`.`LAST_SEEN` AS `last_seen`,`stmts`.`DIGEST` AS `digest` from (`performance_schema`.`events_statements_summary_by_digest` `stmts` join `sys`.`x$ps_digest_95th_percentile_by_avg_us` `top_percentile` on(round(`stmts`.`AVG_TIMER_WAIT` / 1000000,0) >= `top_percentile`.`avg_us`)) order by `stmts`.`AVG_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/statements_with_sorting.frm b/src/main/docker-mariadb/mariadb_data/sys/statements_with_sorting.frm new file mode 100644 index 0000000..60065da --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/statements_with_sorting.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_sort_merges`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_SCAN` AS `sorts_using_scans`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_RANGE` AS `sort_using_range`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +md5=6c3395875de3fc576ff6c0e12479986e +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365605987 +create-version=2 +source=SELECT sys.format_statement(DIGEST_TEXT) AS query,\n SCHEMA_NAME db,\n COUNT_STAR AS exec_count,\n format_pico_time(SUM_TIMER_WAIT) AS total_latency,\n SUM_SORT_MERGE_PASSES AS sort_merge_passes,\n ROUND(IFNULL(SUM_SORT_MERGE_PASSES / NULLIF(COUNT_STAR, 0), 0)) AS avg_sort_merges,\n SUM_SORT_SCAN AS sorts_using_scans,\n SUM_SORT_RANGE AS sort_using_range,\n SUM_SORT_ROWS AS rows_sorted,\n ROUND(IFNULL(SUM_SORT_ROWS / NULLIF(COUNT_STAR, 0), 0)) AS avg_rows_sorted,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE SUM_SORT_ROWS > 0\n ORDER BY SUM_TIMER_WAIT DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_sort_merges`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_SCAN` AS `sorts_using_scans`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_RANGE` AS `sort_using_range`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/statements_with_temp_tables.frm b/src/main/docker-mariadb/mariadb_data/sys/statements_with_temp_tables.frm new file mode 100644 index 0000000..d443437 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/statements_with_temp_tables.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `memory_tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `disk_tmp_tables`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_tmp_tables_per_query`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES`,0),0) * 100,0) AS `tmp_tables_to_disk_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` desc +md5=44565b2be26dc160193a2c4dc7f37af0 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365625048 +create-version=2 +source=SELECT sys.format_statement(DIGEST_TEXT) AS query,\n SCHEMA_NAME as db,\n COUNT_STAR AS exec_count,\n format_pico_time(SUM_TIMER_WAIT) as total_latency,\n SUM_CREATED_TMP_TABLES AS memory_tmp_tables,\n SUM_CREATED_TMP_DISK_TABLES AS disk_tmp_tables,\n ROUND(IFNULL(SUM_CREATED_TMP_TABLES / NULLIF(COUNT_STAR, 0), 0)) AS avg_tmp_tables_per_query,\n ROUND(IFNULL(SUM_CREATED_TMP_DISK_TABLES / NULLIF(SUM_CREATED_TMP_TABLES, 0), 0) * 100) AS tmp_tables_to_disk_pct,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE SUM_CREATED_TMP_TABLES > 0\nORDER BY SUM_CREATED_TMP_DISK_TABLES DESC, SUM_CREATED_TMP_TABLES DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `sys`.`format_statement`(`performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT`) AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,format_pico_time(`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT`) AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `memory_tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `disk_tmp_tables`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_tmp_tables_per_query`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES`,0),0) * 100,0) AS `tmp_tables_to_disk_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/sys_config.MAD b/src/main/docker-mariadb/mariadb_data/sys/sys_config.MAD new file mode 100644 index 0000000..92dbb8b Binary files /dev/null and b/src/main/docker-mariadb/mariadb_data/sys/sys_config.MAD differ diff --git a/src/main/docker-mariadb/mariadb_data/sys/sys_config.MAI b/src/main/docker-mariadb/mariadb_data/sys/sys_config.MAI new file mode 100644 index 0000000..6344f5e Binary files /dev/null and b/src/main/docker-mariadb/mariadb_data/sys/sys_config.MAI differ diff --git a/src/main/docker-mariadb/mariadb_data/sys/sys_config.frm b/src/main/docker-mariadb/mariadb_data/sys/sys_config.frm new file mode 100644 index 0000000..187d8bf Binary files /dev/null and b/src/main/docker-mariadb/mariadb_data/sys/sys_config.frm differ diff --git a/src/main/docker-mariadb/mariadb_data/sys/user_summary.frm b/src/main/docker-mariadb/mariadb_data/sys/user_summary.frm new file mode 100644 index 0000000..0fb1dc8 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/user_summary.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) AS `user`,sum(`stmt`.`total`) AS `statements`,format_pico_time(sum(`stmt`.`total_latency`)) AS `statement_latency`,format_pico_time(ifnull(sum(`stmt`.`total_latency`) / nullif(sum(`stmt`.`total`),0),0)) AS `statement_avg_latency`,sum(`stmt`.`full_scans`) AS `table_scans`,sum(`io`.`ios`) AS `file_ios`,format_pico_time(sum(`io`.`io_latency`)) AS `file_io_latency`,sum(`performance_schema`.`accounts`.`CURRENT_CONNECTIONS`) AS `current_connections`,sum(`performance_schema`.`accounts`.`TOTAL_CONNECTIONS`) AS `total_connections`,count(distinct `performance_schema`.`accounts`.`HOST`) AS `unique_hosts`,`sys`.`format_bytes`(sum(`mem`.`current_allocated`)) AS `current_memory`,`sys`.`format_bytes`(sum(`mem`.`total_allocated`)) AS `total_memory_allocated` from (((`performance_schema`.`accounts` left join `sys`.`x$user_summary_by_statement_latency` `stmt` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `stmt`.`user`)) left join `sys`.`x$user_summary_by_file_io` `io` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `io`.`user`)) left join `sys`.`x$memory_by_user_by_current_bytes` `mem` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `mem`.`user`)) group by if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) order by sum(`stmt`.`total_latency`) desc +md5=8257b4d5fe59ed91cc5df1de9bac74a7 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365758657 +create-version=2 +source=SELECT IF(accounts.user IS NULL, \'background\', accounts.user) AS user,\n SUM(stmt.total) AS statements,\n format_pico_time(SUM(stmt.total_latency)) AS statement_latency,\n format_pico_time(IFNULL(SUM(stmt.total_latency) / NULLIF(SUM(stmt.total), 0), 0)) AS statement_avg_latency,\n SUM(stmt.full_scans) AS table_scans,\n SUM(io.ios) AS file_ios,\n format_pico_time(SUM(io.io_latency)) AS file_io_latency,\n SUM(accounts.current_connections) AS current_connections,\n SUM(accounts.total_connections) AS total_connections,\n COUNT(DISTINCT host) AS unique_hosts,\n sys.format_bytes(SUM(mem.current_allocated)) AS current_memory,\n sys.format_bytes(SUM(mem.total_allocated)) AS total_memory_allocated\n FROM performance_schema.accounts\n LEFT JOIN sys.x$user_summary_by_statement_latency AS stmt ON IF(accounts.user IS NULL, \'background\', accounts.user) = stmt.user\n LEFT JOIN sys.x$user_summary_by_file_io AS io ON IF(accounts.user IS NULL, \'background\', accounts.user) = io.user\n LEFT JOIN sys.x$memory_by_user_by_current_bytes mem ON IF(accounts.user IS NULL, \'background\', accounts.user) = mem.user\n GROUP BY IF(accounts.user IS NULL, \'background\', accounts.user)\n ORDER BY SUM(stmt.total_latency) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) AS `user`,sum(`stmt`.`total`) AS `statements`,format_pico_time(sum(`stmt`.`total_latency`)) AS `statement_latency`,format_pico_time(ifnull(sum(`stmt`.`total_latency`) / nullif(sum(`stmt`.`total`),0),0)) AS `statement_avg_latency`,sum(`stmt`.`full_scans`) AS `table_scans`,sum(`io`.`ios`) AS `file_ios`,format_pico_time(sum(`io`.`io_latency`)) AS `file_io_latency`,sum(`performance_schema`.`accounts`.`CURRENT_CONNECTIONS`) AS `current_connections`,sum(`performance_schema`.`accounts`.`TOTAL_CONNECTIONS`) AS `total_connections`,count(distinct `performance_schema`.`accounts`.`HOST`) AS `unique_hosts`,`sys`.`format_bytes`(sum(`mem`.`current_allocated`)) AS `current_memory`,`sys`.`format_bytes`(sum(`mem`.`total_allocated`)) AS `total_memory_allocated` from (((`performance_schema`.`accounts` left join `sys`.`x$user_summary_by_statement_latency` `stmt` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `stmt`.`user`)) left join `sys`.`x$user_summary_by_file_io` `io` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `io`.`user`)) left join `sys`.`x$memory_by_user_by_current_bytes` `mem` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `mem`.`user`)) group by if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) order by sum(`stmt`.`total_latency`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_file_io.frm b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_file_io.frm new file mode 100644 index 0000000..d8e958f --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_file_io.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR`) AS `ios`,format_pico_time(sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`)) AS `io_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' group by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=10c9c4273d29bed921d4dd363defd2fe +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365664453 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUM(count_star) AS ios,\n format_pico_time(SUM(sum_timer_wait)) AS io_latency\n FROM performance_schema.events_waits_summary_by_user_by_event_name\n WHERE event_name LIKE \'wait/io/file/%\'\n GROUP BY IF(user IS NULL, \'background\', user)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR`) AS `ios`,format_pico_time(sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`)) AS `io_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' group by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_file_io_type.frm b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_file_io_type.frm new file mode 100644 index 0000000..ec8e4ef --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_file_io_type.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file%\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=8967c13925c1911b58ec145ebcce896c +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365645574 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n event_name,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS latency,\n format_pico_time(max_timer_wait) AS max_latency\n FROM performance_schema.events_waits_summary_by_user_by_event_name\n WHERE event_name LIKE \'wait/io/file%\'\n AND count_star > 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file%\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_stages.frm b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_stages.frm new file mode 100644 index 0000000..0099b01 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_stages.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency` from `performance_schema`.`events_stages_summary_by_user_by_event_name` where `performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=5bdb3d6134c2a3f593fd6d0222ac8e65 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365723709 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n event_name,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(avg_timer_wait) AS avg_latency\n FROM performance_schema.events_stages_summary_by_user_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency` from `performance_schema`.`events_stages_summary_by_user_by_event_name` where `performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_statement_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_statement_latency.frm new file mode 100644 index 0000000..5eeddaa --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_statement_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME`)) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=ff951e5566ec0a801b10a1d72f5dd0bf +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365703385 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUM(count_star) AS total,\n format_pico_time(SUM(sum_timer_wait)) AS total_latency,\n format_pico_time(SUM(max_timer_wait)) AS max_latency,\n format_pico_time(SUM(sum_lock_time)) AS lock_latency,\n SUM(sum_rows_sent) AS rows_sent,\n SUM(sum_rows_examined) AS rows_examined,\n SUM(sum_rows_affected) AS rows_affected,\n SUM(sum_no_index_used) + SUM(sum_no_good_index_used) AS full_scans\n FROM performance_schema.events_statements_summary_by_user_by_event_name\n GROUP BY IF(user IS NULL, \'background\', user)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency`,format_pico_time(sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME`)) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_statement_type.frm b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_statement_type.frm new file mode 100644 index 0000000..9260c96 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/user_summary_by_statement_type.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,substring_index(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` where `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=0a9fac42f7b7633e6717f26af91d6b06 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365684213 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUBSTRING_INDEX(event_name, \'/\', -1) AS statement,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(max_timer_wait) AS max_latency,\n format_pico_time(sum_lock_time) AS lock_latency,\n sum_rows_sent AS rows_sent,\n sum_rows_examined AS rows_examined,\n sum_rows_affected AS rows_affected,\n sum_no_index_used + sum_no_good_index_used AS full_scans\n FROM performance_schema.events_statements_summary_by_user_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,substring_index(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,format_pico_time(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` where `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/version.frm b/src/main/docker-mariadb/mariadb_data/sys/version.frm new file mode 100644 index 0000000..bf2757d --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/version.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select \'1.5.2\' AS `sys_version`,version() AS `mysql_version` +md5=4e2508b71654f19bd9553eeabf2918f2 +updatable=0 +algorithm=0 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298364777730 +create-version=2 +source=SELECT \'1.5.2\' AS sys_version,\n version() AS mysql_version; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select \'1.5.2\' AS `sys_version`,version() AS `mysql_version` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/wait_classes_global_by_avg_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/wait_classes_global_by_avg_latency.frm new file mode 100644 index 0000000..b18ef83 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/wait_classes_global_by_avg_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(cast(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) as unsigned)) AS `total_latency`,format_pico_time(min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`)) AS `min_latency`,format_pico_time(ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0)) AS `avg_latency`,format_pico_time(cast(max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) as unsigned)) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) desc +md5=96242bee6930a9ebab75b6776220c644 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365899958 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name,\'/\', 3) AS event_class,\n SUM(COUNT_STAR) AS total,\n format_pico_time(CAST(SUM(sum_timer_wait) AS UNSIGNED)) AS total_latency,\n format_pico_time(MIN(min_timer_wait)) AS min_latency,\n format_pico_time(IFNULL(SUM(sum_timer_wait) / NULLIF(SUM(COUNT_STAR), 0), 0)) AS avg_latency,\n format_pico_time(CAST(MAX(max_timer_wait) AS UNSIGNED)) AS max_latency\n FROM performance_schema.events_waits_summary_global_by_event_name\n WHERE sum_timer_wait > 0\n AND event_name != \'idle\'\n GROUP BY event_class\n ORDER BY IFNULL(SUM(sum_timer_wait) / NULLIF(SUM(COUNT_STAR), 0), 0) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(cast(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) as unsigned)) AS `total_latency`,format_pico_time(min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`)) AS `min_latency`,format_pico_time(ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0)) AS `avg_latency`,format_pico_time(cast(max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) as unsigned)) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/wait_classes_global_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/wait_classes_global_by_latency.frm new file mode 100644 index 0000000..a866564 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/wait_classes_global_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`)) AS `min_latency`,format_pico_time(ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0)) AS `avg_latency`,format_pico_time(max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=4c494df87284af3b937a84ed12339291 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365917876 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name,\'/\', 3) AS event_class,\n SUM(COUNT_STAR) AS total,\n format_pico_time(SUM(sum_timer_wait)) AS total_latency,\n format_pico_time(MIN(min_timer_wait)) min_latency,\n format_pico_time(IFNULL(SUM(sum_timer_wait) / NULLIF(SUM(COUNT_STAR), 0), 0)) AS avg_latency,\n format_pico_time(MAX(max_timer_wait)) AS max_latency\n FROM performance_schema.events_waits_summary_global_by_event_name\n WHERE sum_timer_wait > 0\n AND event_name != \'idle\'\n GROUP BY SUBSTRING_INDEX(event_name,\'/\', 3)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,format_pico_time(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`)) AS `total_latency`,format_pico_time(min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`)) AS `min_latency`,format_pico_time(ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0)) AS `avg_latency`,format_pico_time(max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`)) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/waits_by_host_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/waits_by_host_by_latency.frm new file mode 100644 index 0000000..aac650b --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/waits_by_host_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=d15ee6286e329ddcf149dcb195833962 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365956323 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n event_name AS event,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(avg_timer_wait) AS avg_latency,\n format_pico_time(max_timer_wait) AS max_latency\n FROM performance_schema.events_waits_summary_by_host_by_event_name\n WHERE event_name != \'idle\'\n AND sum_timer_wait > 0\n ORDER BY host, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/waits_by_user_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/waits_by_user_by_latency.frm new file mode 100644 index 0000000..31c82b2 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/waits_by_user_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is not null and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=756ad83e07f144f50c717665485d9461 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365938131 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n event_name AS event,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(avg_timer_wait) AS avg_latency,\n format_pico_time(max_timer_wait) AS max_latency\n FROM performance_schema.events_waits_summary_by_user_by_event_name\n WHERE event_name != \'idle\'\n AND user IS NOT NULL\n AND sum_timer_wait > 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is not null and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/waits_global_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/waits_global_by_latency.frm new file mode 100644 index 0000000..3ec39ef --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/waits_global_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` AS `events`,`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_waits_summary_global_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 order by `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` desc +md5=653d5fed66fefc91b76a1b2bbc2754b3 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365974208 +create-version=2 +source=SELECT event_name AS event,\n count_star AS total,\n format_pico_time(sum_timer_wait) AS total_latency,\n format_pico_time(avg_timer_wait) AS avg_latency,\n format_pico_time(max_timer_wait) AS max_latency\n FROM performance_schema.events_waits_summary_global_by_event_name\n WHERE event_name != \'idle\'\n AND sum_timer_wait > 0\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` AS `events`,`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR` AS `total`,format_pico_time(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,format_pico_time(`performance_schema`.`events_waits_summary_global_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,format_pico_time(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 order by `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary.frm new file mode 100644 index 0000000..78fe63c --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`accounts`.`HOST` is null,\'background\',`performance_schema`.`accounts`.`HOST`) AS `host`,sum(`stmt`.`total`) AS `statements`,sum(`stmt`.`total_latency`) AS `statement_latency`,sum(`stmt`.`total_latency`) / sum(`stmt`.`total`) AS `statement_avg_latency`,sum(`stmt`.`full_scans`) AS `table_scans`,sum(`io`.`ios`) AS `file_ios`,sum(`io`.`io_latency`) AS `file_io_latency`,sum(`performance_schema`.`accounts`.`CURRENT_CONNECTIONS`) AS `current_connections`,sum(`performance_schema`.`accounts`.`TOTAL_CONNECTIONS`) AS `total_connections`,count(distinct `performance_schema`.`accounts`.`USER`) AS `unique_users`,sum(`mem`.`current_allocated`) AS `current_memory`,sum(`mem`.`total_allocated`) AS `total_memory_allocated` from (((`performance_schema`.`accounts` join `sys`.`x$host_summary_by_statement_latency` `stmt` on(`performance_schema`.`accounts`.`HOST` = `stmt`.`host`)) join `sys`.`x$host_summary_by_file_io` `io` on(`performance_schema`.`accounts`.`HOST` = `io`.`host`)) join `sys`.`x$memory_by_host_by_current_bytes` `mem` on(`performance_schema`.`accounts`.`HOST` = `mem`.`host`)) group by if(`performance_schema`.`accounts`.`HOST` is null,\'background\',`performance_schema`.`accounts`.`HOST`) +md5=67e4bc9e1f0f2c08e58833e2d5538896 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365889884 +create-version=2 +source=SELECT IF(accounts.host IS NULL, \'background\', accounts.host) AS host,\n SUM(stmt.total) AS statements,\n SUM(stmt.total_latency) AS statement_latency,\n SUM(stmt.total_latency) / SUM(stmt.total) AS statement_avg_latency,\n SUM(stmt.full_scans) AS table_scans,\n SUM(io.ios) AS file_ios,\n SUM(io.io_latency) AS file_io_latency,\n SUM(accounts.current_connections) AS current_connections,\n SUM(accounts.total_connections) AS total_connections,\n COUNT(DISTINCT accounts.user) AS unique_users,\n SUM(mem.current_allocated) AS current_memory,\n SUM(mem.total_allocated) AS total_memory_allocated\n FROM performance_schema.accounts\n JOIN sys.x$host_summary_by_statement_latency AS stmt ON accounts.host = stmt.host\n JOIN sys.x$host_summary_by_file_io AS io ON accounts.host = io.host\n JOIN sys.x$memory_by_host_by_current_bytes mem ON accounts.host = mem.host\n GROUP BY IF(accounts.host IS NULL, \'background\', accounts.host); +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`accounts`.`HOST` is null,\'background\',`performance_schema`.`accounts`.`HOST`) AS `host`,sum(`stmt`.`total`) AS `statements`,sum(`stmt`.`total_latency`) AS `statement_latency`,sum(`stmt`.`total_latency`) / sum(`stmt`.`total`) AS `statement_avg_latency`,sum(`stmt`.`full_scans`) AS `table_scans`,sum(`io`.`ios`) AS `file_ios`,sum(`io`.`io_latency`) AS `file_io_latency`,sum(`performance_schema`.`accounts`.`CURRENT_CONNECTIONS`) AS `current_connections`,sum(`performance_schema`.`accounts`.`TOTAL_CONNECTIONS`) AS `total_connections`,count(distinct `performance_schema`.`accounts`.`USER`) AS `unique_users`,sum(`mem`.`current_allocated`) AS `current_memory`,sum(`mem`.`total_allocated`) AS `total_memory_allocated` from (((`performance_schema`.`accounts` join `sys`.`x$host_summary_by_statement_latency` `stmt` on(`performance_schema`.`accounts`.`HOST` = `stmt`.`host`)) join `sys`.`x$host_summary_by_file_io` `io` on(`performance_schema`.`accounts`.`HOST` = `io`.`host`)) join `sys`.`x$memory_by_host_by_current_bytes` `mem` on(`performance_schema`.`accounts`.`HOST` = `mem`.`host`)) group by if(`performance_schema`.`accounts`.`HOST` is null,\'background\',`performance_schema`.`accounts`.`HOST`) +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_file_io.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_file_io.frm new file mode 100644 index 0000000..3771bcf --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_file_io.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR`) AS `ios`,sum(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `io_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' group by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=901c381d3d98dbb46350e5bbae184d88 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365808918 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n SUM(count_star) AS ios,\n SUM(sum_timer_wait) AS io_latency\n FROM performance_schema.events_waits_summary_by_host_by_event_name\n WHERE event_name LIKE \'wait/io/file/%\'\n GROUP BY IF(host IS NULL, \'background\', host)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR`) AS `ios`,sum(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `io_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' group by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_file_io_type.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_file_io_type.frm new file mode 100644 index 0000000..0ef0809 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_file_io_type.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` like \'wait/io/file%\' and `performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` > 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=d62d46509ecc2c266e46ec8a89b866f9 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365787472 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n event_name,\n count_star AS total,\n sum_timer_wait AS total_latency,\n max_timer_wait AS max_latency\n FROM performance_schema.events_waits_summary_by_host_by_event_name\n WHERE event_name LIKE \'wait/io/file%\'\n AND count_star > 0\n ORDER BY IF(host IS NULL, \'background\', host), sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` like \'wait/io/file%\' and `performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` > 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_stages.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_stages.frm new file mode 100644 index 0000000..513c8e9 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_stages.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency` from `performance_schema`.`events_stages_summary_by_host_by_event_name` where `performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=fc6f8e38aee0ae855dab711a3ba9f56c +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365866916 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n event_name,\n count_star AS total,\n sum_timer_wait AS total_latency,\n avg_timer_wait AS avg_latency\n FROM performance_schema.events_stages_summary_by_host_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY IF(host IS NULL, \'background\', host), sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_stages_summary_by_host_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency` from `performance_schema`.`events_stages_summary_by_host_by_event_name` where `performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_stages_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_stages_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_statement_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_statement_latency.frm new file mode 100644 index 0000000..69301d5 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_statement_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,max(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=80be376168c9b08aa7c19aaf5ebee747 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365848110 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n SUM(count_star) AS total,\n SUM(sum_timer_wait) AS total_latency,\n MAX(max_timer_wait) AS max_latency,\n SUM(sum_lock_time) AS lock_latency,\n SUM(sum_rows_sent) AS rows_sent,\n SUM(sum_rows_examined) AS rows_examined,\n SUM(sum_rows_affected) AS rows_affected,\n SUM(sum_no_index_used) + SUM(sum_no_good_index_used) AS full_scans\n FROM performance_schema.events_statements_summary_by_host_by_event_name\n GROUP BY IF(host IS NULL, \'background\', host)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,max(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_statement_type.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_statement_type.frm new file mode 100644 index 0000000..2deed6a --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024host_summary_by_statement_type.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,substring_index(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME` AS `lock_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` where `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=f0524c728d74e019a8384c7369a71a51 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365829505 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n SUBSTRING_INDEX(event_name, \'/\', -1) AS statement,\n count_star AS total,\n sum_timer_wait AS total_latency,\n max_timer_wait AS max_latency,\n sum_lock_time AS lock_latency,\n sum_rows_sent AS rows_sent,\n sum_rows_examined AS rows_examined,\n sum_rows_affected AS rows_affected,\n sum_no_index_used + sum_no_good_index_used AS full_scans\n FROM performance_schema.events_statements_summary_by_host_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY IF(host IS NULL, \'background\', host), sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`) AS `host`,substring_index(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_LOCK_TIME` AS `lock_latency`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_host_by_event_name` where `performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_statements_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_statements_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_buffer_stats_by_schema.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_buffer_stats_by_schema.frm new file mode 100644 index 0000000..e299fb7 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_buffer_stats_by_schema.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) AS `allocated`,sum(`ibp`.`DATA_SIZE`) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED`,1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD`,1,NULL)) AS `pages_old`,round(ifnull(sum(`ibp`.`NUMBER_RECORDS`) / nullif(count(distinct `ibp`.`INDEX_NAME`),0),0),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +md5=5520d476400f773f5e963f96dc10e46a +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298364993869 +create-version=2 +source=SELECT IF(LOCATE(\'.\', ibp.table_name) = 0, \'InnoDB System\', REPLACE(SUBSTRING_INDEX(ibp.table_name, \'.\', 1), \'`\', \'\')) AS object_schema,\n SUM(IF(ibp.compressed_size = 0, 16384, compressed_size)) AS allocated,\n SUM(ibp.data_size) AS data,\n COUNT(ibp.page_number) AS pages,\n COUNT(IF(ibp.is_hashed, 1, NULL)) AS pages_hashed,\n COUNT(IF(ibp.is_old, 1, NULL)) AS pages_old,\n ROUND(IFNULL(SUM(ibp.number_records)/NULLIF(COUNT(DISTINCT ibp.index_name), 0), 0)) AS rows_cached\n FROM information_schema.innodb_buffer_page ibp\n WHERE table_name IS NOT NULL\n GROUP BY object_schema\n ORDER BY SUM(IF(ibp.compressed_size = 0, 16384, compressed_size)) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) AS `allocated`,sum(`ibp`.`DATA_SIZE`) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED`,1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD`,1,NULL)) AS `pages_old`,round(ifnull(sum(`ibp`.`NUMBER_RECORDS`) / nullif(count(distinct `ibp`.`INDEX_NAME`),0),0),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_buffer_stats_by_table.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_buffer_stats_by_table.frm new file mode 100644 index 0000000..500298f --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_buffer_stats_by_table.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') AS `object_name`,sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) AS `allocated`,sum(`ibp`.`DATA_SIZE`) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED`,1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD`,1,NULL)) AS `pages_old`,round(ifnull(sum(`ibp`.`NUMBER_RECORDS`) / nullif(count(distinct `ibp`.`INDEX_NAME`),0),0),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')),replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +md5=db96ddcff1919852c2c98356a6e4cc80 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365017984 +create-version=2 +source=SELECT IF(LOCATE(\'.\', ibp.table_name) = 0, \'InnoDB System\', REPLACE(SUBSTRING_INDEX(ibp.table_name, \'.\', 1), \'`\', \'\')) AS object_schema,\n REPLACE(SUBSTRING_INDEX(ibp.table_name, \'.\', -1), \'`\', \'\') AS object_name,\n SUM(IF(ibp.compressed_size = 0, 16384, compressed_size)) AS allocated,\n SUM(ibp.data_size) AS data,\n COUNT(ibp.page_number) AS pages,\n COUNT(IF(ibp.is_hashed, 1, NULL)) AS pages_hashed,\n COUNT(IF(ibp.is_old, 1, NULL)) AS pages_old,\n ROUND(IFNULL(SUM(ibp.number_records)/NULLIF(COUNT(DISTINCT ibp.index_name), 0), 0)) AS rows_cached\n FROM information_schema.innodb_buffer_page ibp\n WHERE table_name IS NOT NULL\n GROUP BY object_schema, object_name\n ORDER BY SUM(IF(ibp.compressed_size = 0, 16384, compressed_size)) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')) AS `object_schema`,replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') AS `object_name`,sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) AS `allocated`,sum(`ibp`.`DATA_SIZE`) AS `data`,count(`ibp`.`PAGE_NUMBER`) AS `pages`,count(if(`ibp`.`IS_HASHED`,1,NULL)) AS `pages_hashed`,count(if(`ibp`.`IS_OLD`,1,NULL)) AS `pages_old`,round(ifnull(sum(`ibp`.`NUMBER_RECORDS`) / nullif(count(distinct `ibp`.`INDEX_NAME`),0),0),0) AS `rows_cached` from `information_schema`.`innodb_buffer_page` `ibp` where `ibp`.`TABLE_NAME` is not null group by if(locate(\'.\',`ibp`.`TABLE_NAME`) = 0,\'InnoDB System\',replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',1),\'`\',\'\')),replace(substring_index(`ibp`.`TABLE_NAME`,\'.\',-1),\'`\',\'\') order by sum(if(`ibp`.`COMPRESSED_SIZE` = 0,16384,`ibp`.`COMPRESSED_SIZE`)) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_lock_waits.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_lock_waits.frm new file mode 100644 index 0000000..dc8a3a7 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024innodb_lock_waits.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `r`.`trx_wait_started` AS `wait_started`,timediff(current_timestamp(),`r`.`trx_wait_started`) AS `wait_age`,timestampdiff(SECOND,`r`.`trx_wait_started`,current_timestamp()) AS `wait_age_secs`,`rl`.`lock_table` AS `locked_table`,`rl`.`lock_index` AS `locked_index`,`rl`.`lock_type` AS `locked_type`,`r`.`trx_id` AS `waiting_trx_id`,`r`.`trx_started` AS `waiting_trx_started`,timediff(current_timestamp(),`r`.`trx_started`) AS `waiting_trx_age`,`r`.`trx_rows_locked` AS `waiting_trx_rows_locked`,`r`.`trx_rows_modified` AS `waiting_trx_rows_modified`,`r`.`trx_mysql_thread_id` AS `waiting_pid`,`r`.`trx_query` AS `waiting_query`,`rl`.`lock_id` AS `waiting_lock_id`,`rl`.`lock_mode` AS `waiting_lock_mode`,`b`.`trx_id` AS `blocking_trx_id`,`b`.`trx_mysql_thread_id` AS `blocking_pid`,`b`.`trx_query` AS `blocking_query`,`bl`.`lock_id` AS `blocking_lock_id`,`bl`.`lock_mode` AS `blocking_lock_mode`,`b`.`trx_started` AS `blocking_trx_started`,timediff(current_timestamp(),`b`.`trx_started`) AS `blocking_trx_age`,`b`.`trx_rows_locked` AS `blocking_trx_rows_locked`,`b`.`trx_rows_modified` AS `blocking_trx_rows_modified`,concat(\'KILL QUERY \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_query`,concat(\'KILL \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_connection` from ((((`information_schema`.`innodb_lock_waits` `w` join `information_schema`.`innodb_trx` `b` on(`b`.`trx_id` = `w`.`blocking_trx_id`)) join `information_schema`.`innodb_trx` `r` on(`r`.`trx_id` = `w`.`requesting_trx_id`)) join `information_schema`.`innodb_locks` `bl` on(`bl`.`lock_id` = `w`.`blocking_lock_id`)) join `information_schema`.`innodb_locks` `rl` on(`rl`.`lock_id` = `w`.`requested_lock_id`)) order by `r`.`trx_wait_started` +md5=a220fb929bcd0fe52730169f87db8aaa +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365048752 +create-version=2 +source=SELECT r.trx_wait_started AS wait_started,\n TIMEDIFF(NOW(), r.trx_wait_started) AS wait_age,\n TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) AS wait_age_secs,\n rl.lock_table AS locked_table,\n rl.lock_index AS locked_index,\n rl.lock_type AS locked_type,\n r.trx_id AS waiting_trx_id,\n r.trx_started as waiting_trx_started,\n TIMEDIFF(NOW(), r.trx_started) AS waiting_trx_age,\n r.trx_rows_locked AS waiting_trx_rows_locked,\n r.trx_rows_modified AS waiting_trx_rows_modified,\n r.trx_mysql_thread_id AS waiting_pid,\n r.trx_query AS waiting_query,\n rl.lock_id AS waiting_lock_id,\n rl.lock_mode AS waiting_lock_mode,\n b.trx_id AS blocking_trx_id,\n b.trx_mysql_thread_id AS blocking_pid,\n b.trx_query AS blocking_query,\n bl.lock_id AS blocking_lock_id,\n bl.lock_mode AS blocking_lock_mode,\n b.trx_started AS blocking_trx_started,\n TIMEDIFF(NOW(), b.trx_started) AS blocking_trx_age,\n b.trx_rows_locked AS blocking_trx_rows_locked,\n b.trx_rows_modified AS blocking_trx_rows_modified,\n CONCAT(\'KILL QUERY \', b.trx_mysql_thread_id) AS sql_kill_blocking_query,\n CONCAT(\'KILL \', b.trx_mysql_thread_id) AS sql_kill_blocking_connection\n FROM information_schema.innodb_lock_waits w\n INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id\n INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id\n INNER JOIN information_schema.innodb_locks bl ON bl.lock_id = w.blocking_lock_id\n INNER JOIN information_schema.innodb_locks rl ON rl.lock_id = w.requested_lock_id\n ORDER BY r.trx_wait_started; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `r`.`trx_wait_started` AS `wait_started`,timediff(current_timestamp(),`r`.`trx_wait_started`) AS `wait_age`,timestampdiff(SECOND,`r`.`trx_wait_started`,current_timestamp()) AS `wait_age_secs`,`rl`.`lock_table` AS `locked_table`,`rl`.`lock_index` AS `locked_index`,`rl`.`lock_type` AS `locked_type`,`r`.`trx_id` AS `waiting_trx_id`,`r`.`trx_started` AS `waiting_trx_started`,timediff(current_timestamp(),`r`.`trx_started`) AS `waiting_trx_age`,`r`.`trx_rows_locked` AS `waiting_trx_rows_locked`,`r`.`trx_rows_modified` AS `waiting_trx_rows_modified`,`r`.`trx_mysql_thread_id` AS `waiting_pid`,`r`.`trx_query` AS `waiting_query`,`rl`.`lock_id` AS `waiting_lock_id`,`rl`.`lock_mode` AS `waiting_lock_mode`,`b`.`trx_id` AS `blocking_trx_id`,`b`.`trx_mysql_thread_id` AS `blocking_pid`,`b`.`trx_query` AS `blocking_query`,`bl`.`lock_id` AS `blocking_lock_id`,`bl`.`lock_mode` AS `blocking_lock_mode`,`b`.`trx_started` AS `blocking_trx_started`,timediff(current_timestamp(),`b`.`trx_started`) AS `blocking_trx_age`,`b`.`trx_rows_locked` AS `blocking_trx_rows_locked`,`b`.`trx_rows_modified` AS `blocking_trx_rows_modified`,concat(\'KILL QUERY \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_query`,concat(\'KILL \',`b`.`trx_mysql_thread_id`) AS `sql_kill_blocking_connection` from ((((`information_schema`.`innodb_lock_waits` `w` join `information_schema`.`innodb_trx` `b` on(`b`.`trx_id` = `w`.`blocking_trx_id`)) join `information_schema`.`innodb_trx` `r` on(`r`.`trx_id` = `w`.`requesting_trx_id`)) join `information_schema`.`innodb_locks` `bl` on(`bl`.`lock_id` = `w`.`blocking_lock_id`)) join `information_schema`.`innodb_locks` `rl` on(`rl`.`lock_id` = `w`.`requested_lock_id`)) order by `r`.`trx_wait_started` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024io_by_thread_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_by_thread_by_latency.frm new file mode 100644 index 0000000..58658bf --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_by_thread_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) AS `user`,sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,min(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,avg(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,max(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` AS `thread_id`,`performance_schema`.`threads`.`PROCESSLIST_ID` AS `processlist_id` from (`performance_schema`.`events_waits_summary_by_thread_by_event_name` left join `performance_schema`.`threads` on(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) where `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT` > 0 group by `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID`,`performance_schema`.`threads`.`PROCESSLIST_ID`,if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) order by sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=29975fcb1586f6e3585d5fd6a60de8e5 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365168996 +create-version=2 +source=SELECT IF(processlist_id IS NULL,\n SUBSTRING_INDEX(name, \'/\', -1),\n CONCAT(processlist_user, \'@\', processlist_host)\n ) user,\n SUM(count_star) total,\n SUM(sum_timer_wait) total_latency,\n MIN(min_timer_wait) min_latency,\n AVG(avg_timer_wait) avg_latency,\n MAX(max_timer_wait) max_latency,\n thread_id,\n processlist_id\n FROM performance_schema.events_waits_summary_by_thread_by_event_name\n LEFT JOIN performance_schema.threads USING (thread_id)\n WHERE event_name LIKE \'wait/io/file/%\'\n AND sum_timer_wait > 0\n GROUP BY thread_id, processlist_id, user\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) AS `user`,sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,min(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,avg(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`AVG_TIMER_WAIT`) AS `avg_latency`,max(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` AS `thread_id`,`performance_schema`.`threads`.`PROCESSLIST_ID` AS `processlist_id` from (`performance_schema`.`events_waits_summary_by_thread_by_event_name` left join `performance_schema`.`threads` on(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) where `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT` > 0 group by `performance_schema`.`events_waits_summary_by_thread_by_event_name`.`THREAD_ID`,`performance_schema`.`threads`.`PROCESSLIST_ID`,if(`performance_schema`.`threads`.`PROCESSLIST_ID` is null,substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),concat(`performance_schema`.`threads`.`PROCESSLIST_USER`,\'@\',`performance_schema`.`threads`.`PROCESSLIST_HOST`)) order by sum(`performance_schema`.`events_waits_summary_by_thread_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_file_by_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_file_by_bytes.frm new file mode 100644 index 0000000..20431e7 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_file_by_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`file_summary_by_instance`.`FILE_NAME` AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` AS `total_read`,ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_READ`,0),0) AS `avg_read`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total_written`,ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE`,0),0.00) AS `avg_write`,`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total`,ifnull(round(100 - `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`,0) * 100,2),0.00) AS `write_pct` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` desc +md5=f8ce3994ef1b5e0e16bba41ce17bacf9 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365188155 +create-version=2 +source=SELECT file_name AS file,\n count_read,\n sum_number_of_bytes_read AS total_read,\n IFNULL(sum_number_of_bytes_read / NULLIF(count_read, 0), 0) AS avg_read,\n count_write,\n sum_number_of_bytes_write AS total_written,\n IFNULL(sum_number_of_bytes_write / NULLIF(count_write, 0), 0.00) AS avg_write,\n sum_number_of_bytes_read + sum_number_of_bytes_write AS total,\n IFNULL(ROUND(100-((sum_number_of_bytes_read/ NULLIF((sum_number_of_bytes_read+sum_number_of_bytes_write), 0))*100), 2), 0.00) AS write_pct\n FROM performance_schema.file_summary_by_instance\n ORDER BY sum_number_of_bytes_read + sum_number_of_bytes_write DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`file_summary_by_instance`.`FILE_NAME` AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` AS `total_read`,ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_READ`,0),0) AS `avg_read`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total_written`,ifnull(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE`,0),0.00) AS `avg_write`,`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total`,ifnull(round(100 - `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`,0) * 100,2),0.00) AS `write_pct` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ` + `performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_file_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_file_by_latency.frm new file mode 100644 index 0000000..8ec327a --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_file_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`file_summary_by_instance`.`FILE_NAME` AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_STAR` AS `total`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_READ` AS `read_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WRITE` AS `write_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_MISC` AS `count_misc`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_MISC` AS `misc_latency` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT` desc +md5=7dd2b8d418cc363387dfae597c25a9f4 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365208596 +create-version=2 +source=SELECT file_name AS file,\n count_star AS total,\n sum_timer_wait AS total_latency,\n count_read,\n sum_timer_read AS read_latency,\n count_write,\n sum_timer_write AS write_latency,\n count_misc,\n sum_timer_misc AS misc_latency\n FROM performance_schema.file_summary_by_instance\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`file_summary_by_instance`.`FILE_NAME` AS `file`,`performance_schema`.`file_summary_by_instance`.`COUNT_STAR` AS `total`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_READ` AS `read_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WRITE` AS `write_latency`,`performance_schema`.`file_summary_by_instance`.`COUNT_MISC` AS `count_misc`,`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_MISC` AS `misc_latency` from `performance_schema`.`file_summary_by_instance` order by `performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_wait_by_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_wait_by_bytes.frm new file mode 100644 index 0000000..267c038 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_wait_by_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`file_summary_by_event_name`.`MIN_TIMER_WAIT` AS `min_latency`,`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` AS `total_read`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total_written`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0) AS `avg_written`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` AS `total_requested` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` desc +md5=4e712a79cda141c8bd60eb52f295fac7 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365228398 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name, \'/\', -2) AS event_name,\n count_star AS total,\n sum_timer_wait AS total_latency,\n min_timer_wait AS min_latency,\n avg_timer_wait AS avg_latency,\n max_timer_wait AS max_latency,\n count_read,\n sum_number_of_bytes_read AS total_read,\n IFNULL(sum_number_of_bytes_read / NULLIF(count_read, 0), 0) AS avg_read,\n count_write,\n sum_number_of_bytes_write AS total_written,\n IFNULL(sum_number_of_bytes_write / NULLIF(count_write, 0), 0) AS avg_written,\n sum_number_of_bytes_write + sum_number_of_bytes_read AS total_requested\n FROM performance_schema.file_summary_by_event_name\n WHERE event_name LIKE \'wait/io/file/%\'\n AND count_star > 0\n ORDER BY sum_number_of_bytes_write + sum_number_of_bytes_read DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`file_summary_by_event_name`.`MIN_TIMER_WAIT` AS `min_latency`,`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` AS `total_read`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total_written`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0) AS `avg_written`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` AS `total_requested` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` + `performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_wait_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_wait_by_latency.frm new file mode 100644 index 0000000..38027b9 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024io_global_by_wait_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_READ` AS `read_latency`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WRITE` AS `write_latency`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_MISC` AS `misc_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` AS `total_read`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total_written`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0) AS `avg_written` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` desc +md5=f414c9e62430c6f80dbc0bc5159ae49c +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365248037 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name, \'/\', -2) AS event_name,\n count_star AS total,\n sum_timer_wait AS total_latency,\n avg_timer_wait AS avg_latency,\n max_timer_wait AS max_latency,\n sum_timer_read AS read_latency,\n sum_timer_write AS write_latency,\n sum_timer_misc AS misc_latency,\n count_read,\n sum_number_of_bytes_read AS total_read,\n IFNULL(sum_number_of_bytes_read / NULLIF(count_read, 0), 0) AS avg_read,\n count_write,\n sum_number_of_bytes_write AS total_written,\n IFNULL(sum_number_of_bytes_write / NULLIF(count_write, 0), 0) AS avg_written\n FROM performance_schema.file_summary_by_event_name\n WHERE event_name LIKE \'wait/io/file/%\'\n AND count_star > 0\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`file_summary_by_event_name`.`EVENT_NAME`,\'/\',-2) AS `event_name`,`performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`file_summary_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`file_summary_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_READ` AS `read_latency`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WRITE` AS `write_latency`,`performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_MISC` AS `misc_latency`,`performance_schema`.`file_summary_by_event_name`.`COUNT_READ` AS `count_read`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` AS `total_read`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_READ` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_READ`,0),0) AS `avg_read`,`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE` AS `count_write`,`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` AS `total_written`,ifnull(`performance_schema`.`file_summary_by_event_name`.`SUM_NUMBER_OF_BYTES_WRITE` / nullif(`performance_schema`.`file_summary_by_event_name`.`COUNT_WRITE`,0),0) AS `avg_written` from `performance_schema`.`file_summary_by_event_name` where `performance_schema`.`file_summary_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' and `performance_schema`.`file_summary_by_event_name`.`COUNT_STAR` > 0 order by `performance_schema`.`file_summary_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024latest_file_io.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024latest_file_io.frm new file mode 100644 index 0000000..dc646d2 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024latest_file_io.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`information_schema`.`processlist`.`ID` is null,concat(substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),\':\',`performance_schema`.`events_waits_history_long`.`THREAD_ID`),concat(`information_schema`.`processlist`.`USER`,\'@\',`information_schema`.`processlist`.`HOST`,\':\',`information_schema`.`processlist`.`ID`)) AS `thread`,`performance_schema`.`events_waits_history_long`.`OBJECT_NAME` AS `file`,`performance_schema`.`events_waits_history_long`.`TIMER_WAIT` AS `latency`,`performance_schema`.`events_waits_history_long`.`OPERATION` AS `operation`,`performance_schema`.`events_waits_history_long`.`NUMBER_OF_BYTES` AS `requested` from ((`performance_schema`.`events_waits_history_long` join `performance_schema`.`threads` on(`performance_schema`.`events_waits_history_long`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) left join `information_schema`.`processlist` on(`performance_schema`.`threads`.`PROCESSLIST_ID` = `information_schema`.`processlist`.`ID`)) where `performance_schema`.`events_waits_history_long`.`OBJECT_NAME` is not null and `performance_schema`.`events_waits_history_long`.`EVENT_NAME` like \'wait/io/file/%\' order by `performance_schema`.`events_waits_history_long`.`TIMER_START` +md5=57dc75fb0d8f65a34b4a54eed20c702c +updatable=0 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365148509 +create-version=2 +source=SELECT IF(id IS NULL,\n CONCAT(SUBSTRING_INDEX(name, \'/\', -1), \':\', thread_id),\n CONCAT(user, \'@\', host, \':\', id)\n ) thread,\n object_name file,\n timer_wait AS latency,\n operation,\n number_of_bytes AS requested\n FROM performance_schema.events_waits_history_long\n JOIN performance_schema.threads USING (thread_id)\n LEFT JOIN information_schema.processlist ON processlist_id = id\n WHERE object_name IS NOT NULL\n AND event_name LIKE \'wait/io/file/%\'\n ORDER BY timer_start; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`information_schema`.`processlist`.`ID` is null,concat(substring_index(`performance_schema`.`threads`.`NAME`,\'/\',-1),\':\',`performance_schema`.`events_waits_history_long`.`THREAD_ID`),concat(`information_schema`.`processlist`.`USER`,\'@\',`information_schema`.`processlist`.`HOST`,\':\',`information_schema`.`processlist`.`ID`)) AS `thread`,`performance_schema`.`events_waits_history_long`.`OBJECT_NAME` AS `file`,`performance_schema`.`events_waits_history_long`.`TIMER_WAIT` AS `latency`,`performance_schema`.`events_waits_history_long`.`OPERATION` AS `operation`,`performance_schema`.`events_waits_history_long`.`NUMBER_OF_BYTES` AS `requested` from ((`performance_schema`.`events_waits_history_long` join `performance_schema`.`threads` on(`performance_schema`.`events_waits_history_long`.`THREAD_ID` = `performance_schema`.`threads`.`THREAD_ID`)) left join `information_schema`.`processlist` on(`performance_schema`.`threads`.`PROCESSLIST_ID` = `information_schema`.`processlist`.`ID`)) where `performance_schema`.`events_waits_history_long`.`OBJECT_NAME` is not null and `performance_schema`.`events_waits_history_long`.`EVENT_NAME` like \'wait/io/file/%\' order by `performance_schema`.`events_waits_history_long`.`TIMER_START` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_host_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_host_by_current_bytes.frm new file mode 100644 index 0000000..7c3714a --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_host_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_allocated`,ifnull(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`),0),0) AS `current_avg_alloc`,max(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_max_alloc`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`) AS `total_allocated` from `performance_schema`.`memory_summary_by_host_by_event_name` group by if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +md5=8830b3e5e58a5783b9ac513f099e5590 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365290597 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n SUM(current_count_used) AS current_count_used,\n SUM(current_number_of_bytes_used) AS current_allocated,\n IFNULL(SUM(current_number_of_bytes_used) / NULLIF(SUM(current_count_used), 0), 0) AS current_avg_alloc,\n MAX(current_number_of_bytes_used) AS current_max_alloc,\n SUM(sum_number_of_bytes_alloc) AS total_allocated\n FROM performance_schema.memory_summary_by_host_by_event_name\n GROUP BY IF(host IS NULL, \'background\', host)\n ORDER BY SUM(current_number_of_bytes_used) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) AS `host`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_allocated`,ifnull(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_COUNT_USED`),0),0) AS `current_avg_alloc`,max(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_max_alloc`,sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`) AS `total_allocated` from `performance_schema`.`memory_summary_by_host_by_event_name` group by if(`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`memory_summary_by_host_by_event_name`.`HOST`) order by sum(`performance_schema`.`memory_summary_by_host_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_thread_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_thread_by_current_bytes.frm new file mode 100644 index 0000000..0f38502 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_thread_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `t`.`THREAD_ID` AS `thread_id`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) AS `user`,sum(`mt`.`CURRENT_COUNT_USED`) AS `current_count_used`,sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_allocated`,ifnull(sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`mt`.`CURRENT_COUNT_USED`),0),0) AS `current_avg_alloc`,max(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_max_alloc`,sum(`mt`.`SUM_NUMBER_OF_BYTES_ALLOC`) AS `total_allocated` from (`performance_schema`.`memory_summary_by_thread_by_event_name` `mt` join `performance_schema`.`threads` `t` on(`mt`.`THREAD_ID` = `t`.`THREAD_ID`)) group by `t`.`THREAD_ID`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) order by sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +md5=090446b6a45d058a679ed900e7b94967 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365311967 +create-version=2 +source=SELECT t.thread_id,\n IF(t.name = \'thread/sql/one_connection\',\n CONCAT(t.processlist_user, \'@\', t.processlist_host),\n REPLACE(t.name, \'thread/\', \'\')) user,\n SUM(mt.current_count_used) AS current_count_used,\n SUM(mt.current_number_of_bytes_used) AS current_allocated,\n IFNULL(SUM(mt.current_number_of_bytes_used) / NULLIF(SUM(current_count_used), 0), 0) AS current_avg_alloc,\n MAX(mt.current_number_of_bytes_used) AS current_max_alloc,\n SUM(mt.sum_number_of_bytes_alloc) AS total_allocated\n FROM performance_schema.memory_summary_by_thread_by_event_name AS mt\n JOIN performance_schema.threads AS t USING (thread_id)\n GROUP BY thread_id, IF(t.name = \'thread/sql/one_connection\',\n CONCAT(t.processlist_user, \'@\', t.processlist_host),\n REPLACE(t.name, \'thread/\', \'\'))\n ORDER BY SUM(mt.current_number_of_bytes_used) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `t`.`THREAD_ID` AS `thread_id`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) AS `user`,sum(`mt`.`CURRENT_COUNT_USED`) AS `current_count_used`,sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_allocated`,ifnull(sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`mt`.`CURRENT_COUNT_USED`),0),0) AS `current_avg_alloc`,max(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_max_alloc`,sum(`mt`.`SUM_NUMBER_OF_BYTES_ALLOC`) AS `total_allocated` from (`performance_schema`.`memory_summary_by_thread_by_event_name` `mt` join `performance_schema`.`threads` `t` on(`mt`.`THREAD_ID` = `t`.`THREAD_ID`)) group by `t`.`THREAD_ID`,if(`t`.`NAME` = \'thread/sql/one_connection\',concat(`t`.`PROCESSLIST_USER`,\'@\',`t`.`PROCESSLIST_HOST`),replace(`t`.`NAME`,\'thread/\',\'\')) order by sum(`mt`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_user_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_user_by_current_bytes.frm new file mode 100644 index 0000000..0ab1157 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_by_user_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_allocated`,ifnull(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`),0),0) AS `current_avg_alloc`,max(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_max_alloc`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`) AS `total_allocated` from `performance_schema`.`memory_summary_by_user_by_event_name` group by if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +md5=2ae77cc919014e48af09252ebc44d28b +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365269965 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUM(current_count_used) AS current_count_used,\n SUM(current_number_of_bytes_used) AS current_allocated,\n IFNULL(SUM(current_number_of_bytes_used) / NULLIF(SUM(current_count_used), 0), 0) AS current_avg_alloc,\n MAX(current_number_of_bytes_used) AS current_max_alloc,\n SUM(sum_number_of_bytes_alloc) AS total_allocated\n FROM performance_schema.memory_summary_by_user_by_event_name\n GROUP BY IF(user IS NULL, \'background\', user)\n ORDER BY SUM(current_number_of_bytes_used) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`) AS `current_count_used`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_allocated`,ifnull(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) / nullif(sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_COUNT_USED`),0),0) AS `current_avg_alloc`,max(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `current_max_alloc`,sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`SUM_NUMBER_OF_BYTES_ALLOC`) AS `total_allocated` from `performance_schema`.`memory_summary_by_user_by_event_name` group by if(`performance_schema`.`memory_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`memory_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`memory_summary_by_user_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_global_by_current_bytes.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_global_by_current_bytes.frm new file mode 100644 index 0000000..78bbad7 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_global_by_current_bytes.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`memory_summary_global_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED` AS `current_count`,`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` AS `current_alloc`,ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED`,0),0) AS `current_avg_alloc`,`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED` AS `high_count`,`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED` AS `high_alloc`,ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED`,0),0) AS `high_avg_alloc` from `performance_schema`.`memory_summary_global_by_event_name` where `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` > 0 order by `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` desc +md5=de3d03378b143da6e5496c82721af418 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365331507 +create-version=2 +source=SELECT event_name,\n current_count_used AS current_count,\n current_number_of_bytes_used AS current_alloc,\n IFNULL(current_number_of_bytes_used / NULLIF(current_count_used, 0), 0) AS current_avg_alloc,\n high_count_used AS high_count,\n high_number_of_bytes_used AS high_alloc,\n IFNULL(high_number_of_bytes_used / NULLIF(high_count_used, 0), 0) AS high_avg_alloc\n FROM performance_schema.memory_summary_global_by_event_name\n WHERE current_number_of_bytes_used > 0\n ORDER BY current_number_of_bytes_used DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`memory_summary_global_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED` AS `current_count`,`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` AS `current_alloc`,ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_COUNT_USED`,0),0) AS `current_avg_alloc`,`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED` AS `high_count`,`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED` AS `high_alloc`,ifnull(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_NUMBER_OF_BYTES_USED` / nullif(`performance_schema`.`memory_summary_global_by_event_name`.`HIGH_COUNT_USED`,0),0) AS `high_avg_alloc` from `performance_schema`.`memory_summary_global_by_event_name` where `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` > 0 order by `performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_global_total.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_global_total.frm new file mode 100644 index 0000000..3e8538f --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024memory_global_total.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select sum(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `total_allocated` from `performance_schema`.`memory_summary_global_by_event_name` +md5=6f943b5a93d4d8b6c06840dbfa5027a9 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365349480 +create-version=2 +source=SELECT SUM(CURRENT_NUMBER_OF_BYTES_USED) total_allocated\n FROM performance_schema.memory_summary_global_by_event_name; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select sum(`performance_schema`.`memory_summary_global_by_event_name`.`CURRENT_NUMBER_OF_BYTES_USED`) AS `total_allocated` from `performance_schema`.`memory_summary_global_by_event_name` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024processlist.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024processlist.frm new file mode 100644 index 0000000..011dcc5 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024processlist.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `pps`.`THREAD_ID` AS `thd_id`,`pps`.`PROCESSLIST_ID` AS `conn_id`,if(`pps`.`NAME` = \'thread/sql/one_connection\',concat(`pps`.`PROCESSLIST_USER`,\'@\',`pps`.`PROCESSLIST_HOST`),replace(`pps`.`NAME`,\'thread/\',\'\')) AS `user`,`pps`.`PROCESSLIST_DB` AS `db`,`pps`.`PROCESSLIST_COMMAND` AS `command`,`pps`.`PROCESSLIST_STATE` AS `state`,`pps`.`PROCESSLIST_TIME` AS `time`,`pps`.`PROCESSLIST_INFO` AS `current_statement`,if(`esc`.`END_EVENT_ID` is null,`esc`.`TIMER_WAIT`,NULL) AS `statement_latency`,if(`esc`.`END_EVENT_ID` is null,round(100 * (`estc`.`WORK_COMPLETED` / `estc`.`WORK_ESTIMATED`),2),NULL) AS `progress`,`esc`.`LOCK_TIME` AS `lock_latency`,`esc`.`ROWS_EXAMINED` AS `rows_examined`,`esc`.`ROWS_SENT` AS `rows_sent`,`esc`.`ROWS_AFFECTED` AS `rows_affected`,`esc`.`CREATED_TMP_TABLES` AS `tmp_tables`,`esc`.`CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,if(`esc`.`NO_GOOD_INDEX_USED` > 0 or `esc`.`NO_INDEX_USED` > 0,\'YES\',\'NO\') AS `full_scan`,if(`esc`.`END_EVENT_ID` is not null,`esc`.`SQL_TEXT`,NULL) AS `last_statement`,if(`esc`.`END_EVENT_ID` is not null,`esc`.`TIMER_WAIT`,NULL) AS `last_statement_latency`,`mem`.`current_allocated` AS `current_memory`,`ewc`.`EVENT_NAME` AS `last_wait`,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',`ewc`.`TIMER_WAIT`) AS `last_wait_latency`,`ewc`.`SOURCE` AS `source`,`etc`.`TIMER_WAIT` AS `trx_latency`,`etc`.`STATE` AS `trx_state`,`etc`.`AUTOCOMMIT` AS `trx_autocommit`,`conattr_pid`.`ATTR_VALUE` AS `pid`,`conattr_progname`.`ATTR_VALUE` AS `program_name` from (((((((`performance_schema`.`threads` `pps` left join `performance_schema`.`events_waits_current` `ewc` on(`pps`.`THREAD_ID` = `ewc`.`THREAD_ID`)) left join `performance_schema`.`events_stages_current` `estc` on(`pps`.`THREAD_ID` = `estc`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `esc` on(`pps`.`THREAD_ID` = `esc`.`THREAD_ID`)) left join `performance_schema`.`events_transactions_current` `etc` on(`pps`.`THREAD_ID` = `etc`.`THREAD_ID`)) left join `sys`.`x$memory_by_thread_by_current_bytes` `mem` on(`pps`.`THREAD_ID` = `mem`.`thread_id`)) left join `performance_schema`.`session_connect_attrs` `conattr_pid` on(`conattr_pid`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_pid`.`ATTR_NAME` = \'_pid\')) left join `performance_schema`.`session_connect_attrs` `conattr_progname` on(`conattr_progname`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_progname`.`ATTR_NAME` = \'program_name\')) order by `pps`.`PROCESSLIST_TIME` desc,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',`ewc`.`TIMER_WAIT`) desc +md5=42b975f81c88e5010bd88768cd426eb7 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298366021699 +create-version=2 +source=SELECT pps.thread_id AS thd_id,\n pps.processlist_id AS conn_id,\n IF(pps.name = \'thread/sql/one_connection\',\n CONCAT(pps.processlist_user, \'@\', pps.processlist_host),\n REPLACE(pps.name, \'thread/\', \'\')) user,\n pps.processlist_db AS db,\n pps.processlist_command AS command,\n pps.processlist_state AS state,\n pps.processlist_time AS time,\n pps.processlist_info AS current_statement,\n IF(esc.end_event_id IS NULL,\n esc.timer_wait,\n NULL) AS statement_latency,\n IF(esc.end_event_id IS NULL,\n ROUND(100 * (estc.work_completed / estc.work_estimated), 2),\n NULL) AS progress,\n esc.lock_time AS lock_latency,\n esc.rows_examined AS rows_examined,\n esc.rows_sent AS rows_sent,\n esc.rows_affected AS rows_affected,\n esc.created_tmp_tables AS tmp_tables,\n esc.created_tmp_disk_tables AS tmp_disk_tables,\n IF(esc.no_good_index_used > 0 OR esc.no_index_used > 0, \'YES\', \'NO\') AS full_scan,\n IF(esc.end_event_id IS NOT NULL,\n esc.sql_text,\n NULL) AS last_statement,\n IF(esc.end_event_id IS NOT NULL,\n esc.timer_wait,\n NULL) AS last_statement_latency,\n mem.current_allocated AS current_memory,\n ewc.event_name AS last_wait,\n IF(ewc.end_event_id IS NULL AND ewc.event_name IS NOT NULL,\n \'Still Waiting\',\n ewc.timer_wait) last_wait_latency,\n ewc.source,\n etc.timer_wait AS trx_latency,\n etc.state AS trx_state,\n etc.autocommit AS trx_autocommit,\n conattr_pid.attr_value as pid,\n conattr_progname.attr_value as program_name\n FROM performance_schema.threads AS pps\n LEFT JOIN performance_schema.events_waits_current AS ewc USING (thread_id)\n LEFT JOIN performance_schema.events_stages_current AS estc USING (thread_id)\n LEFT JOIN performance_schema.events_statements_current AS esc USING (thread_id)\n LEFT JOIN performance_schema.events_transactions_current AS etc USING (thread_id)\n LEFT JOIN sys.x$memory_by_thread_by_current_bytes AS mem USING (thread_id)\n LEFT JOIN performance_schema.session_connect_attrs AS conattr_pid\n ON conattr_pid.processlist_id=pps.processlist_id and conattr_pid.attr_name=\'_pid\'\n LEFT JOIN performance_schema.session_connect_attrs AS conattr_progname\n ON conattr_progname.processlist_id=pps.processlist_id and conattr_progname.attr_name=\'program_name\'\n ORDER BY pps.processlist_time DESC, last_wait_latency DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `pps`.`THREAD_ID` AS `thd_id`,`pps`.`PROCESSLIST_ID` AS `conn_id`,if(`pps`.`NAME` = \'thread/sql/one_connection\',concat(`pps`.`PROCESSLIST_USER`,\'@\',`pps`.`PROCESSLIST_HOST`),replace(`pps`.`NAME`,\'thread/\',\'\')) AS `user`,`pps`.`PROCESSLIST_DB` AS `db`,`pps`.`PROCESSLIST_COMMAND` AS `command`,`pps`.`PROCESSLIST_STATE` AS `state`,`pps`.`PROCESSLIST_TIME` AS `time`,`pps`.`PROCESSLIST_INFO` AS `current_statement`,if(`esc`.`END_EVENT_ID` is null,`esc`.`TIMER_WAIT`,NULL) AS `statement_latency`,if(`esc`.`END_EVENT_ID` is null,round(100 * (`estc`.`WORK_COMPLETED` / `estc`.`WORK_ESTIMATED`),2),NULL) AS `progress`,`esc`.`LOCK_TIME` AS `lock_latency`,`esc`.`ROWS_EXAMINED` AS `rows_examined`,`esc`.`ROWS_SENT` AS `rows_sent`,`esc`.`ROWS_AFFECTED` AS `rows_affected`,`esc`.`CREATED_TMP_TABLES` AS `tmp_tables`,`esc`.`CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,if(`esc`.`NO_GOOD_INDEX_USED` > 0 or `esc`.`NO_INDEX_USED` > 0,\'YES\',\'NO\') AS `full_scan`,if(`esc`.`END_EVENT_ID` is not null,`esc`.`SQL_TEXT`,NULL) AS `last_statement`,if(`esc`.`END_EVENT_ID` is not null,`esc`.`TIMER_WAIT`,NULL) AS `last_statement_latency`,`mem`.`current_allocated` AS `current_memory`,`ewc`.`EVENT_NAME` AS `last_wait`,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',`ewc`.`TIMER_WAIT`) AS `last_wait_latency`,`ewc`.`SOURCE` AS `source`,`etc`.`TIMER_WAIT` AS `trx_latency`,`etc`.`STATE` AS `trx_state`,`etc`.`AUTOCOMMIT` AS `trx_autocommit`,`conattr_pid`.`ATTR_VALUE` AS `pid`,`conattr_progname`.`ATTR_VALUE` AS `program_name` from (((((((`performance_schema`.`threads` `pps` left join `performance_schema`.`events_waits_current` `ewc` on(`pps`.`THREAD_ID` = `ewc`.`THREAD_ID`)) left join `performance_schema`.`events_stages_current` `estc` on(`pps`.`THREAD_ID` = `estc`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `esc` on(`pps`.`THREAD_ID` = `esc`.`THREAD_ID`)) left join `performance_schema`.`events_transactions_current` `etc` on(`pps`.`THREAD_ID` = `etc`.`THREAD_ID`)) left join `sys`.`x$memory_by_thread_by_current_bytes` `mem` on(`pps`.`THREAD_ID` = `mem`.`thread_id`)) left join `performance_schema`.`session_connect_attrs` `conattr_pid` on(`conattr_pid`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_pid`.`ATTR_NAME` = \'_pid\')) left join `performance_schema`.`session_connect_attrs` `conattr_progname` on(`conattr_progname`.`PROCESSLIST_ID` = `pps`.`PROCESSLIST_ID` and `conattr_progname`.`ATTR_NAME` = \'program_name\')) order by `pps`.`PROCESSLIST_TIME` desc,if(`ewc`.`END_EVENT_ID` is null and `ewc`.`EVENT_NAME` is not null,\'Still Waiting\',`ewc`.`TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_digest_95th_percentile_by_avg_us.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_digest_95th_percentile_by_avg_us.frm new file mode 100644 index 0000000..bf04a71 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_digest_95th_percentile_by_avg_us.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `s2`.`avg_us` AS `avg_us`,ifnull(sum(`s1`.`cnt`) / nullif((select count(0) from `performance_schema`.`events_statements_summary_by_digest`),0),0) AS `percentile` from (`sys`.`x$ps_digest_avg_latency_distribution` `s1` join `sys`.`x$ps_digest_avg_latency_distribution` `s2` on(`s1`.`avg_us` <= `s2`.`avg_us`)) group by `s2`.`avg_us` having ifnull(sum(`s1`.`cnt`) / nullif((select count(0) from `performance_schema`.`events_statements_summary_by_digest`),0),0) > 0.95 order by ifnull(sum(`s1`.`cnt`) / nullif((select count(0) from `performance_schema`.`events_statements_summary_by_digest`),0),0) limit 1 +md5=9d4c91bfffb022a4413bbda627e2c569 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365569725 +create-version=2 +source=SELECT s2.avg_us avg_us,\n IFNULL(SUM(s1.cnt)/NULLIF((SELECT COUNT(*) FROM performance_schema.events_statements_summary_by_digest), 0), 0) percentile\n FROM sys.x$ps_digest_avg_latency_distribution AS s1\n JOIN sys.x$ps_digest_avg_latency_distribution AS s2\n ON s1.avg_us <= s2.avg_us\n GROUP BY s2.avg_us\nHAVING IFNULL(SUM(s1.cnt)/NULLIF((SELECT COUNT(*) FROM performance_schema.events_statements_summary_by_digest), 0), 0) > 0.95\n ORDER BY percentile\n LIMIT 1; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `s2`.`avg_us` AS `avg_us`,ifnull(sum(`s1`.`cnt`) / nullif((select count(0) from `performance_schema`.`events_statements_summary_by_digest`),0),0) AS `percentile` from (`sys`.`x$ps_digest_avg_latency_distribution` `s1` join `sys`.`x$ps_digest_avg_latency_distribution` `s2` on(`s1`.`avg_us` <= `s2`.`avg_us`)) group by `s2`.`avg_us` having ifnull(sum(`s1`.`cnt`) / nullif((select count(0) from `performance_schema`.`events_statements_summary_by_digest`),0),0) > 0.95 order by ifnull(sum(`s1`.`cnt`) / nullif((select count(0) from `performance_schema`.`events_statements_summary_by_digest`),0),0) limit 1 +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_digest_avg_latency_distribution.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_digest_avg_latency_distribution.frm new file mode 100644 index 0000000..ce0ff4c --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_digest_avg_latency_distribution.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select count(0) AS `cnt`,round(`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT` / 1000000,0) AS `avg_us` from `performance_schema`.`events_statements_summary_by_digest` group by round(`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT` / 1000000,0) +md5=90f26794b9a8e64fa4b20b5972595230 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365550206 +create-version=2 +source=SELECT COUNT(*) cnt,\n ROUND(avg_timer_wait/1000000) AS avg_us\n FROM performance_schema.events_statements_summary_by_digest\n GROUP BY avg_us; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select count(0) AS `cnt`,round(`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT` / 1000000,0) AS `avg_us` from `performance_schema`.`events_statements_summary_by_digest` group by round(`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT` / 1000000,0) +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_schema_table_statistics_io.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_schema_table_statistics_io.frm new file mode 100644 index 0000000..bbeefc1 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024ps_schema_table_statistics_io.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `extract_schema_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `table_schema`,`extract_table_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `table_name`,sum(`performance_schema`.`file_summary_by_instance`.`COUNT_READ`) AS `count_read`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ`) AS `sum_number_of_bytes_read`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_READ`) AS `sum_timer_read`,sum(`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE`) AS `count_write`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `sum_number_of_bytes_write`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WRITE`) AS `sum_timer_write`,sum(`performance_schema`.`file_summary_by_instance`.`COUNT_MISC`) AS `count_misc`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_MISC`) AS `sum_timer_misc` from `performance_schema`.`file_summary_by_instance` group by `extract_schema_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`),`extract_table_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) +md5=1cbc73d7bab53e57bfac894c9697fd30 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365378080 +create-version=2 +source=SELECT extract_schema_from_file_name(file_name) AS table_schema,\n extract_table_from_file_name(file_name) AS table_name,\n SUM(count_read) AS count_read,\n SUM(sum_number_of_bytes_read) AS sum_number_of_bytes_read,\n SUM(sum_timer_read) AS sum_timer_read,\n SUM(count_write) AS count_write,\n SUM(sum_number_of_bytes_write) AS sum_number_of_bytes_write,\n SUM(sum_timer_write) AS sum_timer_write,\n SUM(count_misc) AS count_misc,\n SUM(sum_timer_misc) AS sum_timer_misc\n FROM performance_schema.file_summary_by_instance\n GROUP BY table_schema, table_name; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `extract_schema_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `table_schema`,`extract_table_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) AS `table_name`,sum(`performance_schema`.`file_summary_by_instance`.`COUNT_READ`) AS `count_read`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_READ`) AS `sum_number_of_bytes_read`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_READ`) AS `sum_timer_read`,sum(`performance_schema`.`file_summary_by_instance`.`COUNT_WRITE`) AS `count_write`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_NUMBER_OF_BYTES_WRITE`) AS `sum_number_of_bytes_write`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_WRITE`) AS `sum_timer_write`,sum(`performance_schema`.`file_summary_by_instance`.`COUNT_MISC`) AS `count_misc`,sum(`performance_schema`.`file_summary_by_instance`.`SUM_TIMER_MISC`) AS `sum_timer_misc` from `performance_schema`.`file_summary_by_instance` group by `extract_schema_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`),`extract_table_from_file_name`(`performance_schema`.`file_summary_by_instance`.`FILE_NAME`) +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_flattened_keys.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_flattened_keys.frm new file mode 100644 index 0000000..44ac639 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_flattened_keys.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `information_schema`.`statistics`.`TABLE_SCHEMA` AS `table_schema`,`information_schema`.`statistics`.`TABLE_NAME` AS `table_name`,`information_schema`.`statistics`.`INDEX_NAME` AS `index_name`,max(`information_schema`.`statistics`.`NON_UNIQUE`) AS `non_unique`,max(if(`information_schema`.`statistics`.`SUB_PART` is null,0,1)) AS `subpart_exists`,group_concat(`information_schema`.`statistics`.`COLUMN_NAME` order by `information_schema`.`statistics`.`SEQ_IN_INDEX` ASC separator \',\') AS `index_columns` from `information_schema`.`statistics` where `information_schema`.`statistics`.`INDEX_TYPE` = \'BTREE\' and `information_schema`.`statistics`.`TABLE_SCHEMA` not in (\'mysql\',\'sys\',\'INFORMATION_SCHEMA\',\'PERFORMANCE_SCHEMA\') group by `information_schema`.`statistics`.`TABLE_SCHEMA`,`information_schema`.`statistics`.`TABLE_NAME`,`information_schema`.`statistics`.`INDEX_NAME` +md5=481b526d4164504abbca5c600860e8c5 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365083098 +create-version=2 +source=SELECT\n TABLE_SCHEMA,\n TABLE_NAME,\n INDEX_NAME,\n MAX(NON_UNIQUE) AS non_unique,\n MAX(IF(SUB_PART IS NULL, 0, 1)) AS subpart_exists,\n GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS index_columns\n FROM INFORMATION_SCHEMA.STATISTICS\n WHERE\n INDEX_TYPE=\'BTREE\'\n AND TABLE_SCHEMA NOT IN (\'mysql\', \'sys\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\')\n GROUP BY\n TABLE_SCHEMA, TABLE_NAME, INDEX_NAME; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `information_schema`.`statistics`.`TABLE_SCHEMA` AS `table_schema`,`information_schema`.`statistics`.`TABLE_NAME` AS `table_name`,`information_schema`.`statistics`.`INDEX_NAME` AS `index_name`,max(`information_schema`.`statistics`.`NON_UNIQUE`) AS `non_unique`,max(if(`information_schema`.`statistics`.`SUB_PART` is null,0,1)) AS `subpart_exists`,group_concat(`information_schema`.`statistics`.`COLUMN_NAME` order by `information_schema`.`statistics`.`SEQ_IN_INDEX` ASC separator \',\') AS `index_columns` from `information_schema`.`statistics` where `information_schema`.`statistics`.`INDEX_TYPE` = \'BTREE\' and `information_schema`.`statistics`.`TABLE_SCHEMA` not in (\'mysql\',\'sys\',\'INFORMATION_SCHEMA\',\'PERFORMANCE_SCHEMA\') group by `information_schema`.`statistics`.`TABLE_SCHEMA`,`information_schema`.`statistics`.`TABLE_NAME`,`information_schema`.`statistics`.`INDEX_NAME` +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_index_statistics.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_index_statistics.frm new file mode 100644 index 0000000..3441ac3 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_index_statistics.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `table_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `table_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` AS `index_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_FETCH` AS `rows_selected`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_FETCH` AS `select_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_INSERT` AS `rows_inserted`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT` AS `insert_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_UPDATE` AS `rows_updated`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_UPDATE` AS `update_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_DELETE` AS `rows_deleted`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT` AS `delete_latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is not null order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT` desc +md5=5844eee9b3fc97a283ca9e10e94f2258 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365368826 +create-version=2 +source=SELECT OBJECT_SCHEMA AS table_schema,\n OBJECT_NAME AS table_name,\n INDEX_NAME as index_name,\n COUNT_FETCH AS rows_selected,\n SUM_TIMER_FETCH AS select_latency,\n COUNT_INSERT AS rows_inserted,\n SUM_TIMER_INSERT AS insert_latency,\n COUNT_UPDATE AS rows_updated,\n SUM_TIMER_UPDATE AS update_latency,\n COUNT_DELETE AS rows_deleted,\n SUM_TIMER_INSERT AS delete_latency\n FROM performance_schema.table_io_waits_summary_by_index_usage\n WHERE index_name IS NOT NULL\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `table_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `table_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` AS `index_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_FETCH` AS `rows_selected`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_FETCH` AS `select_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_INSERT` AS `rows_inserted`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT` AS `insert_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_UPDATE` AS `rows_updated`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_UPDATE` AS `update_latency`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_DELETE` AS `rows_deleted`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_INSERT` AS `delete_latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is not null order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_lock_waits.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_lock_waits.frm new file mode 100644 index 0000000..8a38fa0 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_lock_waits.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `g`.`OBJECT_SCHEMA` AS `object_schema`,`g`.`OBJECT_NAME` AS `object_name`,`pt`.`THREAD_ID` AS `waiting_thread_id`,`pt`.`PROCESSLIST_ID` AS `waiting_pid`,`sys`.`ps_thread_account`(`p`.`OWNER_THREAD_ID`) AS `waiting_account`,`p`.`LOCK_TYPE` AS `waiting_lock_type`,`p`.`LOCK_DURATION` AS `waiting_lock_duration`,`pt`.`PROCESSLIST_INFO` AS `waiting_query`,`pt`.`PROCESSLIST_TIME` AS `waiting_query_secs`,`ps`.`ROWS_AFFECTED` AS `waiting_query_rows_affected`,`ps`.`ROWS_EXAMINED` AS `waiting_query_rows_examined`,`gt`.`THREAD_ID` AS `blocking_thread_id`,`gt`.`PROCESSLIST_ID` AS `blocking_pid`,`sys`.`ps_thread_account`(`g`.`OWNER_THREAD_ID`) AS `blocking_account`,`g`.`LOCK_TYPE` AS `blocking_lock_type`,`g`.`LOCK_DURATION` AS `blocking_lock_duration`,concat(\'KILL QUERY \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_query`,concat(\'KILL \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_connection` from (((((`performance_schema`.`metadata_locks` `g` join `performance_schema`.`metadata_locks` `p` on(`g`.`OBJECT_TYPE` = `p`.`OBJECT_TYPE` and `g`.`OBJECT_SCHEMA` = `p`.`OBJECT_SCHEMA` and `g`.`OBJECT_NAME` = `p`.`OBJECT_NAME` and `g`.`LOCK_STATUS` = \'GRANTED\' and `p`.`LOCK_STATUS` = \'PENDING\')) join `performance_schema`.`threads` `gt` on(`g`.`OWNER_THREAD_ID` = `gt`.`THREAD_ID`)) join `performance_schema`.`threads` `pt` on(`p`.`OWNER_THREAD_ID` = `pt`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `gs` on(`g`.`OWNER_THREAD_ID` = `gs`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `ps` on(`p`.`OWNER_THREAD_ID` = `ps`.`THREAD_ID`)) where `g`.`OBJECT_TYPE` = \'TABLE\' +md5=9bde9b7793a0d9ebd1c022f6e54a7ae2 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365481729 +create-version=2 +source=SELECT g.object_schema AS object_schema,\n g.object_name AS object_name,\n pt.thread_id AS waiting_thread_id,\n pt.processlist_id AS waiting_pid,\n sys.ps_thread_account(p.owner_thread_id) AS waiting_account,\n p.lock_type AS waiting_lock_type,\n p.lock_duration AS waiting_lock_duration,\n pt.processlist_info AS waiting_query,\n pt.processlist_time AS waiting_query_secs,\n ps.rows_affected AS waiting_query_rows_affected,\n ps.rows_examined AS waiting_query_rows_examined,\n gt.thread_id AS blocking_thread_id,\n gt.processlist_id AS blocking_pid,\n sys.ps_thread_account(g.owner_thread_id) AS blocking_account,\n g.lock_type AS blocking_lock_type,\n g.lock_duration AS blocking_lock_duration,\n CONCAT(\'KILL QUERY \', gt.processlist_id) AS sql_kill_blocking_query,\n CONCAT(\'KILL \', gt.processlist_id) AS sql_kill_blocking_connection\n FROM performance_schema.metadata_locks g\n INNER JOIN performance_schema.metadata_locks p\n ON g.object_type = p.object_type\n AND g.object_schema = p.object_schema\n AND g.object_name = p.object_name\n AND g.lock_status = \'GRANTED\'\n AND p.lock_status = \'PENDING\'\n INNER JOIN performance_schema.threads gt ON g.owner_thread_id = gt.thread_id\n INNER JOIN performance_schema.threads pt ON p.owner_thread_id = pt.thread_id\n LEFT JOIN performance_schema.events_statements_current gs ON g.owner_thread_id = gs.thread_id\n LEFT JOIN performance_schema.events_statements_current ps ON p.owner_thread_id = ps.thread_id\n WHERE g.object_type = \'TABLE\'; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `g`.`OBJECT_SCHEMA` AS `object_schema`,`g`.`OBJECT_NAME` AS `object_name`,`pt`.`THREAD_ID` AS `waiting_thread_id`,`pt`.`PROCESSLIST_ID` AS `waiting_pid`,`sys`.`ps_thread_account`(`p`.`OWNER_THREAD_ID`) AS `waiting_account`,`p`.`LOCK_TYPE` AS `waiting_lock_type`,`p`.`LOCK_DURATION` AS `waiting_lock_duration`,`pt`.`PROCESSLIST_INFO` AS `waiting_query`,`pt`.`PROCESSLIST_TIME` AS `waiting_query_secs`,`ps`.`ROWS_AFFECTED` AS `waiting_query_rows_affected`,`ps`.`ROWS_EXAMINED` AS `waiting_query_rows_examined`,`gt`.`THREAD_ID` AS `blocking_thread_id`,`gt`.`PROCESSLIST_ID` AS `blocking_pid`,`sys`.`ps_thread_account`(`g`.`OWNER_THREAD_ID`) AS `blocking_account`,`g`.`LOCK_TYPE` AS `blocking_lock_type`,`g`.`LOCK_DURATION` AS `blocking_lock_duration`,concat(\'KILL QUERY \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_query`,concat(\'KILL \',`gt`.`PROCESSLIST_ID`) AS `sql_kill_blocking_connection` from (((((`performance_schema`.`metadata_locks` `g` join `performance_schema`.`metadata_locks` `p` on(`g`.`OBJECT_TYPE` = `p`.`OBJECT_TYPE` and `g`.`OBJECT_SCHEMA` = `p`.`OBJECT_SCHEMA` and `g`.`OBJECT_NAME` = `p`.`OBJECT_NAME` and `g`.`LOCK_STATUS` = \'GRANTED\' and `p`.`LOCK_STATUS` = \'PENDING\')) join `performance_schema`.`threads` `gt` on(`g`.`OWNER_THREAD_ID` = `gt`.`THREAD_ID`)) join `performance_schema`.`threads` `pt` on(`p`.`OWNER_THREAD_ID` = `pt`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `gs` on(`g`.`OWNER_THREAD_ID` = `gs`.`THREAD_ID`)) left join `performance_schema`.`events_statements_current` `ps` on(`p`.`OWNER_THREAD_ID` = `ps`.`THREAD_ID`)) where `g`.`OBJECT_TYPE` = \'TABLE\' +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_statistics.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_statistics.frm new file mode 100644 index 0000000..955ea7e --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_statistics.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,`pst`.`SUM_TIMER_WAIT` AS `total_latency`,`pst`.`COUNT_FETCH` AS `rows_fetched`,`pst`.`SUM_TIMER_FETCH` AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,`pst`.`SUM_TIMER_INSERT` AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,`pst`.`SUM_TIMER_UPDATE` AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,`pst`.`SUM_TIMER_DELETE` AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`fsbi`.`sum_number_of_bytes_read` AS `io_read`,`fsbi`.`sum_timer_read` AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`fsbi`.`sum_number_of_bytes_write` AS `io_write`,`fsbi`.`sum_timer_write` AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,`fsbi`.`sum_timer_misc` AS `io_misc_latency` from (`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +md5=bc53e98a227086ffb2e6185030b7fc95 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365405911 +create-version=2 +source=SELECT pst.object_schema AS table_schema,\n pst.object_name AS table_name,\n pst.sum_timer_wait AS total_latency,\n pst.count_fetch AS rows_fetched,\n pst.sum_timer_fetch AS fetch_latency,\n pst.count_insert AS rows_inserted,\n pst.sum_timer_insert AS insert_latency,\n pst.count_update AS rows_updated,\n pst.sum_timer_update AS update_latency,\n pst.count_delete AS rows_deleted,\n pst.sum_timer_delete AS delete_latency,\n fsbi.count_read AS io_read_requests,\n fsbi.sum_number_of_bytes_read AS io_read,\n fsbi.sum_timer_read AS io_read_latency,\n fsbi.count_write AS io_write_requests,\n fsbi.sum_number_of_bytes_write AS io_write,\n fsbi.sum_timer_write AS io_write_latency,\n fsbi.count_misc AS io_misc_requests,\n fsbi.sum_timer_misc AS io_misc_latency\n FROM performance_schema.table_io_waits_summary_by_table AS pst\n LEFT JOIN x$ps_schema_table_statistics_io AS fsbi\n ON pst.object_schema = fsbi.table_schema\n AND pst.object_name = fsbi.table_name\n ORDER BY pst.sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,`pst`.`SUM_TIMER_WAIT` AS `total_latency`,`pst`.`COUNT_FETCH` AS `rows_fetched`,`pst`.`SUM_TIMER_FETCH` AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,`pst`.`SUM_TIMER_INSERT` AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,`pst`.`SUM_TIMER_UPDATE` AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,`pst`.`SUM_TIMER_DELETE` AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`fsbi`.`sum_number_of_bytes_read` AS `io_read`,`fsbi`.`sum_timer_read` AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`fsbi`.`sum_number_of_bytes_write` AS `io_write`,`fsbi`.`sum_timer_write` AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,`fsbi`.`sum_timer_misc` AS `io_misc_latency` from (`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_statistics_with_buffer.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_statistics_with_buffer.frm new file mode 100644 index 0000000..bd3f7c0 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_table_statistics_with_buffer.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,`pst`.`COUNT_FETCH` AS `rows_fetched`,`pst`.`SUM_TIMER_FETCH` AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,`pst`.`SUM_TIMER_INSERT` AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,`pst`.`SUM_TIMER_UPDATE` AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,`pst`.`SUM_TIMER_DELETE` AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`fsbi`.`sum_number_of_bytes_read` AS `io_read`,`fsbi`.`sum_timer_read` AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`fsbi`.`sum_number_of_bytes_write` AS `io_write`,`fsbi`.`sum_timer_write` AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,`fsbi`.`sum_timer_misc` AS `io_misc_latency`,`ibp`.`allocated` AS `innodb_buffer_allocated`,`ibp`.`data` AS `innodb_buffer_data`,`ibp`.`allocated` - `ibp`.`data` AS `innodb_buffer_free`,`ibp`.`pages` AS `innodb_buffer_pages`,`ibp`.`pages_hashed` AS `innodb_buffer_pages_hashed`,`ibp`.`pages_old` AS `innodb_buffer_pages_old`,`ibp`.`rows_cached` AS `innodb_buffer_rows_cached` from ((`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) left join `sys`.`x$innodb_buffer_stats_by_table` `ibp` on(`pst`.`OBJECT_SCHEMA` = `ibp`.`object_schema` and `pst`.`OBJECT_NAME` = `ibp`.`object_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +md5=6028a2655b55a68d15450953ebc6eaae +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365428216 +create-version=2 +source=SELECT pst.object_schema AS table_schema,\n pst.object_name AS table_name,\n pst.count_fetch AS rows_fetched,\n pst.sum_timer_fetch AS fetch_latency,\n pst.count_insert AS rows_inserted,\n pst.sum_timer_insert AS insert_latency,\n pst.count_update AS rows_updated,\n pst.sum_timer_update AS update_latency,\n pst.count_delete AS rows_deleted,\n pst.sum_timer_delete AS delete_latency,\n fsbi.count_read AS io_read_requests,\n fsbi.sum_number_of_bytes_read AS io_read,\n fsbi.sum_timer_read AS io_read_latency,\n fsbi.count_write AS io_write_requests,\n fsbi.sum_number_of_bytes_write AS io_write,\n fsbi.sum_timer_write AS io_write_latency,\n fsbi.count_misc AS io_misc_requests,\n fsbi.sum_timer_misc AS io_misc_latency,\n ibp.allocated AS innodb_buffer_allocated,\n ibp.data AS innodb_buffer_data,\n (ibp.allocated - ibp.data) AS innodb_buffer_free,\n ibp.pages AS innodb_buffer_pages,\n ibp.pages_hashed AS innodb_buffer_pages_hashed,\n ibp.pages_old AS innodb_buffer_pages_old,\n ibp.rows_cached AS innodb_buffer_rows_cached\n FROM performance_schema.table_io_waits_summary_by_table AS pst\n LEFT JOIN x$ps_schema_table_statistics_io AS fsbi\n ON pst.object_schema = fsbi.table_schema\n AND pst.object_name = fsbi.table_name\n LEFT JOIN sys.x$innodb_buffer_stats_by_table AS ibp\n ON pst.object_schema = ibp.object_schema\n AND pst.object_name = ibp.object_name\n ORDER BY pst.sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `pst`.`OBJECT_SCHEMA` AS `table_schema`,`pst`.`OBJECT_NAME` AS `table_name`,`pst`.`COUNT_FETCH` AS `rows_fetched`,`pst`.`SUM_TIMER_FETCH` AS `fetch_latency`,`pst`.`COUNT_INSERT` AS `rows_inserted`,`pst`.`SUM_TIMER_INSERT` AS `insert_latency`,`pst`.`COUNT_UPDATE` AS `rows_updated`,`pst`.`SUM_TIMER_UPDATE` AS `update_latency`,`pst`.`COUNT_DELETE` AS `rows_deleted`,`pst`.`SUM_TIMER_DELETE` AS `delete_latency`,`fsbi`.`count_read` AS `io_read_requests`,`fsbi`.`sum_number_of_bytes_read` AS `io_read`,`fsbi`.`sum_timer_read` AS `io_read_latency`,`fsbi`.`count_write` AS `io_write_requests`,`fsbi`.`sum_number_of_bytes_write` AS `io_write`,`fsbi`.`sum_timer_write` AS `io_write_latency`,`fsbi`.`count_misc` AS `io_misc_requests`,`fsbi`.`sum_timer_misc` AS `io_misc_latency`,`ibp`.`allocated` AS `innodb_buffer_allocated`,`ibp`.`data` AS `innodb_buffer_data`,`ibp`.`allocated` - `ibp`.`data` AS `innodb_buffer_free`,`ibp`.`pages` AS `innodb_buffer_pages`,`ibp`.`pages_hashed` AS `innodb_buffer_pages_hashed`,`ibp`.`pages_old` AS `innodb_buffer_pages_old`,`ibp`.`rows_cached` AS `innodb_buffer_rows_cached` from ((`performance_schema`.`table_io_waits_summary_by_table` `pst` left join `sys`.`x$ps_schema_table_statistics_io` `fsbi` on(`pst`.`OBJECT_SCHEMA` = `fsbi`.`table_schema` and `pst`.`OBJECT_NAME` = `fsbi`.`table_name`)) left join `sys`.`x$innodb_buffer_stats_by_table` `ibp` on(`pst`.`OBJECT_SCHEMA` = `ibp`.`object_schema` and `pst`.`OBJECT_NAME` = `ibp`.`object_name`)) order by `pst`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_tables_with_full_table_scans.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_tables_with_full_table_scans.frm new file mode 100644 index 0000000..00f637a --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024schema_tables_with_full_table_scans.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `object_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `object_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` AS `rows_full_scanned`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT` AS `latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is null and `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` > 0 order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` desc +md5=2a4a59ec97eaf3d4a3b09f58b253c058 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365447034 +create-version=2 +source=SELECT object_schema,\n object_name,\n count_read AS rows_full_scanned,\n sum_timer_wait AS latency\n FROM performance_schema.table_io_waits_summary_by_index_usage\n WHERE index_name IS NULL\n AND count_read > 0\n ORDER BY count_read DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_SCHEMA` AS `object_schema`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`OBJECT_NAME` AS `object_name`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` AS `rows_full_scanned`,`performance_schema`.`table_io_waits_summary_by_index_usage`.`SUM_TIMER_WAIT` AS `latency` from `performance_schema`.`table_io_waits_summary_by_index_usage` where `performance_schema`.`table_io_waits_summary_by_index_usage`.`INDEX_NAME` is null and `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` > 0 order by `performance_schema`.`table_io_waits_summary_by_index_usage`.`COUNT_READ` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024session.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024session.frm new file mode 100644 index 0000000..6de621d --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024session.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `x$processlist`.`thd_id` AS `thd_id`,`x$processlist`.`conn_id` AS `conn_id`,`x$processlist`.`user` AS `user`,`x$processlist`.`db` AS `db`,`x$processlist`.`command` AS `command`,`x$processlist`.`state` AS `state`,`x$processlist`.`time` AS `time`,`x$processlist`.`current_statement` AS `current_statement`,`x$processlist`.`statement_latency` AS `statement_latency`,`x$processlist`.`progress` AS `progress`,`x$processlist`.`lock_latency` AS `lock_latency`,`x$processlist`.`rows_examined` AS `rows_examined`,`x$processlist`.`rows_sent` AS `rows_sent`,`x$processlist`.`rows_affected` AS `rows_affected`,`x$processlist`.`tmp_tables` AS `tmp_tables`,`x$processlist`.`tmp_disk_tables` AS `tmp_disk_tables`,`x$processlist`.`full_scan` AS `full_scan`,`x$processlist`.`last_statement` AS `last_statement`,`x$processlist`.`last_statement_latency` AS `last_statement_latency`,`x$processlist`.`current_memory` AS `current_memory`,`x$processlist`.`last_wait` AS `last_wait`,`x$processlist`.`last_wait_latency` AS `last_wait_latency`,`x$processlist`.`source` AS `source`,`x$processlist`.`trx_latency` AS `trx_latency`,`x$processlist`.`trx_state` AS `trx_state`,`x$processlist`.`trx_autocommit` AS `trx_autocommit`,`x$processlist`.`pid` AS `pid`,`x$processlist`.`program_name` AS `program_name` from `sys`.`x$processlist` where `x$processlist`.`conn_id` is not null and `x$processlist`.`command` <> \'Daemon\' +md5=20277a58098b6112ed36c5879dbb6c33 +updatable=0 +algorithm=0 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298366045400 +create-version=2 +source=SELECT * FROM sys.x$processlist\nWHERE conn_id IS NOT NULL AND command != \'Daemon\'; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `x$processlist`.`thd_id` AS `thd_id`,`x$processlist`.`conn_id` AS `conn_id`,`x$processlist`.`user` AS `user`,`x$processlist`.`db` AS `db`,`x$processlist`.`command` AS `command`,`x$processlist`.`state` AS `state`,`x$processlist`.`time` AS `time`,`x$processlist`.`current_statement` AS `current_statement`,`x$processlist`.`statement_latency` AS `statement_latency`,`x$processlist`.`progress` AS `progress`,`x$processlist`.`lock_latency` AS `lock_latency`,`x$processlist`.`rows_examined` AS `rows_examined`,`x$processlist`.`rows_sent` AS `rows_sent`,`x$processlist`.`rows_affected` AS `rows_affected`,`x$processlist`.`tmp_tables` AS `tmp_tables`,`x$processlist`.`tmp_disk_tables` AS `tmp_disk_tables`,`x$processlist`.`full_scan` AS `full_scan`,`x$processlist`.`last_statement` AS `last_statement`,`x$processlist`.`last_statement_latency` AS `last_statement_latency`,`x$processlist`.`current_memory` AS `current_memory`,`x$processlist`.`last_wait` AS `last_wait`,`x$processlist`.`last_wait_latency` AS `last_wait_latency`,`x$processlist`.`source` AS `source`,`x$processlist`.`trx_latency` AS `trx_latency`,`x$processlist`.`trx_state` AS `trx_state`,`x$processlist`.`trx_autocommit` AS `trx_autocommit`,`x$processlist`.`pid` AS `pid`,`x$processlist`.`program_name` AS `program_name` from `sys`.`x$processlist` where `x$processlist`.`conn_id` is not null and `x$processlist`.`command` <> \'Daemon\' +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024statement_analysis.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024statement_analysis.frm new file mode 100644 index 0000000..d1a1cc9 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024statement_analysis.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,if(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `err_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warn_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_LOCK_TIME` AS `lock_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` AS `rows_affected`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_affected_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen` from `performance_schema`.`events_statements_summary_by_digest` order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +md5=c4d19fab63f1a07088959a488e8921e5 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365500945 +create-version=2 +source=SELECT DIGEST_TEXT AS query,\n SCHEMA_NAME AS db,\n IF(SUM_NO_GOOD_INDEX_USED > 0 OR SUM_NO_INDEX_USED > 0, \'*\', \'\') AS full_scan,\n COUNT_STAR AS exec_count,\n SUM_ERRORS AS err_count,\n SUM_WARNINGS AS warn_count,\n SUM_TIMER_WAIT AS total_latency,\n MAX_TIMER_WAIT AS max_latency,\n AVG_TIMER_WAIT AS avg_latency,\n SUM_LOCK_TIME AS lock_latency,\n SUM_ROWS_SENT AS rows_sent,\n ROUND(IFNULL(SUM_ROWS_SENT / NULLIF(COUNT_STAR, 0), 0)) AS rows_sent_avg,\n SUM_ROWS_EXAMINED AS rows_examined,\n ROUND(IFNULL(SUM_ROWS_EXAMINED / NULLIF(COUNT_STAR, 0), 0)) AS rows_examined_avg,\n SUM_ROWS_AFFECTED AS rows_affected,\n ROUND(IFNULL(SUM_ROWS_AFFECTED / NULLIF(COUNT_STAR, 0), 0)) AS rows_affected_avg,\n SUM_CREATED_TMP_TABLES AS tmp_tables,\n SUM_CREATED_TMP_DISK_TABLES AS tmp_disk_tables,\n SUM_SORT_ROWS AS rows_sorted,\n SUM_SORT_MERGE_PASSES AS sort_merge_passes,\n DIGEST AS digest,\n FIRST_SEEN AS first_seen,\n LAST_SEEN as last_seen\n FROM performance_schema.events_statements_summary_by_digest\nORDER BY SUM_TIMER_WAIT DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,if(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `err_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warn_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`events_statements_summary_by_digest`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_LOCK_TIME` AS `lock_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` AS `rows_affected`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_AFFECTED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `rows_affected_avg`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `tmp_disk_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen` from `performance_schema`.`events_statements_summary_by_digest` order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_errors_or_warnings.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_errors_or_warnings.frm new file mode 100644 index 0000000..11e3901 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_errors_or_warnings.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `errors`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `error_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warnings`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `warning_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` desc +md5=490874cb895cee05cc7f8d813cd41ae8 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365521123 +create-version=2 +source=SELECT DIGEST_TEXT AS query,\n SCHEMA_NAME as db,\n COUNT_STAR AS exec_count,\n SUM_ERRORS AS errors,\n IFNULL(SUM_ERRORS / NULLIF(COUNT_STAR, 0), 0) * 100 as error_pct,\n SUM_WARNINGS AS warnings,\n IFNULL(SUM_WARNINGS / NULLIF(COUNT_STAR, 0), 0) * 100 as warning_pct,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE SUM_ERRORS > 0\n OR SUM_WARNINGS > 0\nORDER BY SUM_ERRORS DESC, SUM_WARNINGS DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` AS `errors`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `error_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` AS `warnings`,ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100 AS `warning_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_ERRORS` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_WARNINGS` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_full_table_scans.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_full_table_scans.frm new file mode 100644 index 0000000..e744900 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_full_table_scans.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` AS `no_index_used_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` AS `no_good_index_used_count`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) AS `no_index_used_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_sent_avg`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where (`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0) and `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` not like \'SHOW%\' order by round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +md5=25b42c369cee614420db6a77c1fc10c6 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365540406 +create-version=2 +source=SELECT DIGEST_TEXT AS query,\n SCHEMA_NAME as db,\n COUNT_STAR AS exec_count,\n SUM_TIMER_WAIT AS total_latency,\n SUM_NO_INDEX_USED AS no_index_used_count,\n SUM_NO_GOOD_INDEX_USED AS no_good_index_used_count,\n ROUND(IFNULL(SUM_NO_INDEX_USED / NULLIF(COUNT_STAR, 0), 0) * 100) AS no_index_used_pct,\n SUM_ROWS_SENT AS rows_sent,\n SUM_ROWS_EXAMINED AS rows_examined,\n ROUND(SUM_ROWS_SENT/COUNT_STAR) AS rows_sent_avg,\n ROUND(SUM_ROWS_EXAMINED/COUNT_STAR) AS rows_examined_avg,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE (SUM_NO_INDEX_USED > 0\n OR SUM_NO_GOOD_INDEX_USED > 0)\n AND DIGEST_TEXT NOT LIKE \'SHOW%\'\n ORDER BY no_index_used_pct DESC, total_latency DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` AS `no_index_used_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` AS `no_good_index_used_count`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) AS `no_index_used_pct`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_SENT` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_sent_avg`,round(`performance_schema`.`events_statements_summary_by_digest`.`SUM_ROWS_EXAMINED` / `performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0) AS `rows_examined_avg`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where (`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` > 0 or `performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_GOOD_INDEX_USED` > 0) and `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` not like \'SHOW%\' order by round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_NO_INDEX_USED` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0) * 100,0) desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_runtimes_in_95th_percentile.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_runtimes_in_95th_percentile.frm new file mode 100644 index 0000000..869de3d --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_runtimes_in_95th_percentile.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `stmts`.`DIGEST_TEXT` AS `query`,`stmts`.`SCHEMA_NAME` AS `db`,if(`stmts`.`SUM_NO_GOOD_INDEX_USED` > 0 or `stmts`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`stmts`.`COUNT_STAR` AS `exec_count`,`stmts`.`SUM_ERRORS` AS `err_count`,`stmts`.`SUM_WARNINGS` AS `warn_count`,`stmts`.`SUM_TIMER_WAIT` AS `total_latency`,`stmts`.`MAX_TIMER_WAIT` AS `max_latency`,`stmts`.`AVG_TIMER_WAIT` AS `avg_latency`,`stmts`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`stmts`.`SUM_ROWS_SENT` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`stmts`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`stmts`.`SUM_ROWS_EXAMINED` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`stmts`.`FIRST_SEEN` AS `first_seen`,`stmts`.`LAST_SEEN` AS `last_seen`,`stmts`.`DIGEST` AS `digest` from (`performance_schema`.`events_statements_summary_by_digest` `stmts` join `sys`.`x$ps_digest_95th_percentile_by_avg_us` `top_percentile` on(round(`stmts`.`AVG_TIMER_WAIT` / 1000000,0) >= `top_percentile`.`avg_us`)) order by `stmts`.`AVG_TIMER_WAIT` desc +md5=8cd6ce405be77ab4980ea412c943b553 +updatable=0 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365596128 +create-version=2 +source=SELECT DIGEST_TEXT AS query,\n SCHEMA_NAME AS db,\n IF(SUM_NO_GOOD_INDEX_USED > 0 OR SUM_NO_INDEX_USED > 0, \'*\', \'\') AS full_scan,\n COUNT_STAR AS exec_count,\n SUM_ERRORS AS err_count,\n SUM_WARNINGS AS warn_count,\n SUM_TIMER_WAIT AS total_latency,\n MAX_TIMER_WAIT AS max_latency,\n AVG_TIMER_WAIT AS avg_latency,\n SUM_ROWS_SENT AS rows_sent,\n ROUND(IFNULL(SUM_ROWS_SENT / NULLIF(COUNT_STAR, 0), 0)) AS rows_sent_avg,\n SUM_ROWS_EXAMINED AS rows_examined,\n ROUND(IFNULL(SUM_ROWS_EXAMINED / NULLIF(COUNT_STAR, 0), 0)) AS rows_examined_avg,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest stmts\n JOIN sys.x$ps_digest_95th_percentile_by_avg_us AS top_percentile\n ON ROUND(stmts.avg_timer_wait/1000000) >= top_percentile.avg_us\n ORDER BY AVG_TIMER_WAIT DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `stmts`.`DIGEST_TEXT` AS `query`,`stmts`.`SCHEMA_NAME` AS `db`,if(`stmts`.`SUM_NO_GOOD_INDEX_USED` > 0 or `stmts`.`SUM_NO_INDEX_USED` > 0,\'*\',\'\') AS `full_scan`,`stmts`.`COUNT_STAR` AS `exec_count`,`stmts`.`SUM_ERRORS` AS `err_count`,`stmts`.`SUM_WARNINGS` AS `warn_count`,`stmts`.`SUM_TIMER_WAIT` AS `total_latency`,`stmts`.`MAX_TIMER_WAIT` AS `max_latency`,`stmts`.`AVG_TIMER_WAIT` AS `avg_latency`,`stmts`.`SUM_ROWS_SENT` AS `rows_sent`,round(ifnull(`stmts`.`SUM_ROWS_SENT` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_sent_avg`,`stmts`.`SUM_ROWS_EXAMINED` AS `rows_examined`,round(ifnull(`stmts`.`SUM_ROWS_EXAMINED` / nullif(`stmts`.`COUNT_STAR`,0),0),0) AS `rows_examined_avg`,`stmts`.`FIRST_SEEN` AS `first_seen`,`stmts`.`LAST_SEEN` AS `last_seen`,`stmts`.`DIGEST` AS `digest` from (`performance_schema`.`events_statements_summary_by_digest` `stmts` join `sys`.`x$ps_digest_95th_percentile_by_avg_us` `top_percentile` on(round(`stmts`.`AVG_TIMER_WAIT` / 1000000,0) >= `top_percentile`.`avg_us`)) order by `stmts`.`AVG_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_sorting.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_sorting.frm new file mode 100644 index 0000000..d8237d8 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_sorting.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_sort_merges`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_SCAN` AS `sorts_using_scans`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_RANGE` AS `sort_using_range`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +md5=8b01a28d1860db00b058ebea2c26a640 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365615252 +create-version=2 +source=SELECT DIGEST_TEXT AS query,\n SCHEMA_NAME db,\n COUNT_STAR AS exec_count,\n SUM_TIMER_WAIT AS total_latency,\n SUM_SORT_MERGE_PASSES AS sort_merge_passes,\n ROUND(IFNULL(SUM_SORT_MERGE_PASSES / NULLIF(COUNT_STAR, 0), 0)) AS avg_sort_merges,\n SUM_SORT_SCAN AS sorts_using_scans,\n SUM_SORT_RANGE AS sort_using_range,\n SUM_SORT_ROWS AS rows_sorted,\n ROUND(IFNULL(SUM_SORT_ROWS / NULLIF(COUNT_STAR, 0), 0)) AS avg_rows_sorted,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE SUM_SORT_ROWS > 0\n ORDER BY SUM_TIMER_WAIT DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` AS `sort_merge_passes`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_MERGE_PASSES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_sort_merges`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_SCAN` AS `sorts_using_scans`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_RANGE` AS `sort_using_range`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` AS `rows_sorted`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_rows_sorted`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_SORT_ROWS` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_temp_tables.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_temp_tables.frm new file mode 100644 index 0000000..7de46b4 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024statements_with_temp_tables.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `memory_tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `disk_tmp_tables`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_tmp_tables_per_query`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES`,0),0) * 100,0) AS `tmp_tables_to_disk_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` desc +md5=f5cd01b57e735775d233ed9f8cb24232 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365634661 +create-version=2 +source=SELECT DIGEST_TEXT AS query,\n SCHEMA_NAME as db,\n COUNT_STAR AS exec_count,\n SUM_TIMER_WAIT as total_latency,\n SUM_CREATED_TMP_TABLES AS memory_tmp_tables,\n SUM_CREATED_TMP_DISK_TABLES AS disk_tmp_tables,\n ROUND(IFNULL(SUM_CREATED_TMP_TABLES / NULLIF(COUNT_STAR, 0), 0)) AS avg_tmp_tables_per_query,\n ROUND(IFNULL(SUM_CREATED_TMP_DISK_TABLES / NULLIF(SUM_CREATED_TMP_TABLES, 0), 0) * 100) AS tmp_tables_to_disk_pct,\n FIRST_SEEN as first_seen,\n LAST_SEEN as last_seen,\n DIGEST AS digest\n FROM performance_schema.events_statements_summary_by_digest\n WHERE SUM_CREATED_TMP_TABLES > 0\nORDER BY SUM_CREATED_TMP_DISK_TABLES DESC, SUM_CREATED_TMP_TABLES DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`events_statements_summary_by_digest`.`DIGEST_TEXT` AS `query`,`performance_schema`.`events_statements_summary_by_digest`.`SCHEMA_NAME` AS `db`,`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR` AS `exec_count`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` AS `memory_tmp_tables`,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` AS `disk_tmp_tables`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`COUNT_STAR`,0),0),0) AS `avg_tmp_tables_per_query`,round(ifnull(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` / nullif(`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES`,0),0) * 100,0) AS `tmp_tables_to_disk_pct`,`performance_schema`.`events_statements_summary_by_digest`.`FIRST_SEEN` AS `first_seen`,`performance_schema`.`events_statements_summary_by_digest`.`LAST_SEEN` AS `last_seen`,`performance_schema`.`events_statements_summary_by_digest`.`DIGEST` AS `digest` from `performance_schema`.`events_statements_summary_by_digest` where `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` > 0 order by `performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_DISK_TABLES` desc,`performance_schema`.`events_statements_summary_by_digest`.`SUM_CREATED_TMP_TABLES` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary.frm new file mode 100644 index 0000000..5916331 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) AS `user`,sum(`stmt`.`total`) AS `statements`,sum(`stmt`.`total_latency`) AS `statement_latency`,ifnull(sum(`stmt`.`total_latency`) / nullif(sum(`stmt`.`total`),0),0) AS `statement_avg_latency`,sum(`stmt`.`full_scans`) AS `table_scans`,sum(`io`.`ios`) AS `file_ios`,sum(`io`.`io_latency`) AS `file_io_latency`,sum(`performance_schema`.`accounts`.`CURRENT_CONNECTIONS`) AS `current_connections`,sum(`performance_schema`.`accounts`.`TOTAL_CONNECTIONS`) AS `total_connections`,count(distinct `performance_schema`.`accounts`.`HOST`) AS `unique_hosts`,sum(`mem`.`current_allocated`) AS `current_memory`,sum(`mem`.`total_allocated`) AS `total_memory_allocated` from (((`performance_schema`.`accounts` left join `sys`.`x$user_summary_by_statement_latency` `stmt` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `stmt`.`user`)) left join `sys`.`x$user_summary_by_file_io` `io` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `io`.`user`)) left join `sys`.`x$memory_by_user_by_current_bytes` `mem` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `mem`.`user`)) group by if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) order by sum(`stmt`.`total_latency`) desc +md5=82e3b4c9bcfc7fb791db32600a9deaed +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365768460 +create-version=2 +source=SELECT IF(accounts.user IS NULL, \'background\', accounts.user) AS user,\n SUM(stmt.total) AS statements,\n SUM(stmt.total_latency) AS statement_latency,\n IFNULL(SUM(stmt.total_latency) / NULLIF(SUM(stmt.total), 0), 0) AS statement_avg_latency,\n SUM(stmt.full_scans) AS table_scans,\n SUM(io.ios) AS file_ios,\n SUM(io.io_latency) AS file_io_latency,\n SUM(accounts.current_connections) AS current_connections,\n SUM(accounts.total_connections) AS total_connections,\n COUNT(DISTINCT host) AS unique_hosts,\n SUM(mem.current_allocated) AS current_memory,\n SUM(mem.total_allocated) AS total_memory_allocated\n FROM performance_schema.accounts\n LEFT JOIN sys.x$user_summary_by_statement_latency AS stmt ON IF(accounts.user IS NULL, \'background\', accounts.user) = stmt.user\n LEFT JOIN sys.x$user_summary_by_file_io AS io ON IF(accounts.user IS NULL, \'background\', accounts.user) = io.user\n LEFT JOIN sys.x$memory_by_user_by_current_bytes mem ON IF(accounts.user IS NULL, \'background\', accounts.user) = mem.user\n GROUP BY IF(accounts.user IS NULL, \'background\', accounts.user)\n ORDER BY SUM(stmt.total_latency) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) AS `user`,sum(`stmt`.`total`) AS `statements`,sum(`stmt`.`total_latency`) AS `statement_latency`,ifnull(sum(`stmt`.`total_latency`) / nullif(sum(`stmt`.`total`),0),0) AS `statement_avg_latency`,sum(`stmt`.`full_scans`) AS `table_scans`,sum(`io`.`ios`) AS `file_ios`,sum(`io`.`io_latency`) AS `file_io_latency`,sum(`performance_schema`.`accounts`.`CURRENT_CONNECTIONS`) AS `current_connections`,sum(`performance_schema`.`accounts`.`TOTAL_CONNECTIONS`) AS `total_connections`,count(distinct `performance_schema`.`accounts`.`HOST`) AS `unique_hosts`,sum(`mem`.`current_allocated`) AS `current_memory`,sum(`mem`.`total_allocated`) AS `total_memory_allocated` from (((`performance_schema`.`accounts` left join `sys`.`x$user_summary_by_statement_latency` `stmt` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `stmt`.`user`)) left join `sys`.`x$user_summary_by_file_io` `io` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `io`.`user`)) left join `sys`.`x$memory_by_user_by_current_bytes` `mem` on(if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) = `mem`.`user`)) group by if(`performance_schema`.`accounts`.`USER` is null,\'background\',`performance_schema`.`accounts`.`USER`) order by sum(`stmt`.`total_latency`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_file_io.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_file_io.frm new file mode 100644 index 0000000..76a2728 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_file_io.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR`) AS `ios`,sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `io_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' group by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=6df37c787ef9fce933608c125a1387fb +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365673192 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUM(count_star) AS ios,\n SUM(sum_timer_wait) AS io_latency\n FROM performance_schema.events_waits_summary_by_user_by_event_name\n WHERE event_name LIKE \'wait/io/file/%\'\n GROUP BY IF(user IS NULL, \'background\', user)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR`) AS `ios`,sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `io_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file/%\' group by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_file_io_type.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_file_io_type.frm new file mode 100644 index 0000000..61af77e --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_file_io_type.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `latency`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file%\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=090cb2483f78e6606559dbe59df50262 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365655758 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n event_name,\n count_star AS total,\n sum_timer_wait AS latency,\n max_timer_wait AS max_latency\n FROM performance_schema.events_waits_summary_by_user_by_event_name\n WHERE event_name LIKE \'wait/io/file%\'\n AND count_star > 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `latency`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` like \'wait/io/file%\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_stages.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_stages.frm new file mode 100644 index 0000000..4b3bb41 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_stages.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency` from `performance_schema`.`events_stages_summary_by_user_by_event_name` where `performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=fd4b252d26f75f39a2cc024c0274015f +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365739874 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n event_name,\n count_star AS total,\n sum_timer_wait AS total_latency,\n avg_timer_wait AS avg_latency\n FROM performance_schema.events_stages_summary_by_user_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`EVENT_NAME` AS `event_name`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_stages_summary_by_user_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency` from `performance_schema`.`events_stages_summary_by_user_by_event_name` where `performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_stages_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_stages_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_statement_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_statement_latency.frm new file mode 100644 index 0000000..37e62ee --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_statement_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=8a66e66ddf15b2e1bac716c31092459e +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365712676 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUM(count_star) AS total,\n SUM(sum_timer_wait) AS total_latency,\n SUM(max_timer_wait) AS max_latency,\n SUM(sum_lock_time) AS lock_latency,\n SUM(sum_rows_sent) AS rows_sent,\n SUM(sum_rows_examined) AS rows_examined,\n SUM(sum_rows_affected) AS rows_affected,\n SUM(sum_no_index_used) + SUM(sum_no_good_index_used) AS full_scans\n FROM performance_schema.events_statements_summary_by_user_by_event_name\n GROUP BY IF(user IS NULL, \'background\', user)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME`) AS `lock_latency`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT`) AS `rows_sent`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED`) AS `rows_examined`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED`) AS `rows_affected`,sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED`) + sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED`) AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` group by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) order by sum(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_statement_type.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_statement_type.frm new file mode 100644 index 0000000..edc5cff --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024user_summary_by_statement_type.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,substring_index(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME` AS `lock_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` where `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=ccc4b90f7417d82d85b9dc522b56d654 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365693615 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n SUBSTRING_INDEX(event_name, \'/\', -1) AS statement,\n count_star AS total,\n sum_timer_wait AS total_latency,\n max_timer_wait AS max_latency,\n sum_lock_time AS lock_latency,\n sum_rows_sent AS rows_sent,\n sum_rows_examined AS rows_examined,\n sum_rows_affected AS rows_affected,\n sum_no_index_used + sum_no_good_index_used AS full_scans\n FROM performance_schema.events_statements_summary_by_user_by_event_name\n WHERE sum_timer_wait != 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`) AS `user`,substring_index(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`EVENT_NAME`,\'/\',-1) AS `statement`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_LOCK_TIME` AS `lock_latency`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_SENT` AS `rows_sent`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_EXAMINED` AS `rows_examined`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_ROWS_AFFECTED` AS `rows_affected`,`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_INDEX_USED` + `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_NO_GOOD_INDEX_USED` AS `full_scans` from `performance_schema`.`events_statements_summary_by_user_by_event_name` where `performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` <> 0 order by if(`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_statements_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_statements_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024wait_classes_global_by_avg_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024wait_classes_global_by_avg_latency.frm new file mode 100644 index 0000000..f6de2ac --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024wait_classes_global_by_avg_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) AS `avg_latency`,max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) desc +md5=a2b68acb933f339797a5f792e65b55f3 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365908902 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name,\'/\', 3) AS event_class,\n SUM(COUNT_STAR) AS total,\n SUM(sum_timer_wait) AS total_latency,\n MIN(min_timer_wait) AS min_latency,\n IFNULL(SUM(sum_timer_wait) / NULLIF(SUM(COUNT_STAR), 0), 0) AS avg_latency,\n MAX(max_timer_wait) AS max_latency\n FROM performance_schema.events_waits_summary_global_by_event_name\n WHERE sum_timer_wait > 0\n AND event_name != \'idle\'\n GROUP BY event_class\n ORDER BY IFNULL(SUM(sum_timer_wait) / NULLIF(SUM(COUNT_STAR), 0), 0) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) AS `avg_latency`,max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024wait_classes_global_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024wait_classes_global_by_latency.frm new file mode 100644 index 0000000..fcc21dd --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024wait_classes_global_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) AS `avg_latency`,max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) desc +md5=b107bc5e0b1c409651fc3872b551fbd6 +updatable=0 +algorithm=2 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365928022 +create-version=2 +source=SELECT SUBSTRING_INDEX(event_name,\'/\', 3) AS event_class,\n SUM(COUNT_STAR) AS total,\n SUM(sum_timer_wait) AS total_latency,\n MIN(min_timer_wait) AS min_latency,\n IFNULL(SUM(sum_timer_wait) / NULLIF(SUM(COUNT_STAR), 0), 0) AS avg_latency,\n MAX(max_timer_wait) AS max_latency\n FROM performance_schema.events_waits_summary_global_by_event_name\n WHERE sum_timer_wait > 0\n AND event_name != \'idle\'\n GROUP BY SUBSTRING_INDEX(event_name,\'/\', 3)\n ORDER BY SUM(sum_timer_wait) DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) AS `event_class`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`) AS `total`,sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) AS `total_latency`,min(`performance_schema`.`events_waits_summary_global_by_event_name`.`MIN_TIMER_WAIT`) AS `min_latency`,ifnull(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) / nullif(sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR`),0),0) AS `avg_latency`,max(`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT`) AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 and `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' group by substring_index(`performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME`,\'/\',3) order by sum(`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT`) desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_by_host_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_by_host_by_latency.frm new file mode 100644 index 0000000..89e4553 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_by_host_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +md5=6db496ac0e3e32dd8e3ca80c584f8ccd +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365965391 +create-version=2 +source=SELECT IF(host IS NULL, \'background\', host) AS host,\n event_name AS event,\n count_star AS total,\n sum_timer_wait AS total_latency,\n avg_timer_wait AS avg_latency,\n max_timer_wait AS max_latency\n FROM performance_schema.events_waits_summary_by_host_by_event_name\n WHERE event_name != \'idle\'\n AND sum_timer_wait > 0\n ORDER BY host, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`) AS `host`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_waits_summary_by_host_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_host_by_event_name` where `performance_schema`.`events_waits_summary_by_host_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST` is null,\'background\',`performance_schema`.`events_waits_summary_by_host_by_event_name`.`HOST`),`performance_schema`.`events_waits_summary_by_host_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_by_user_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_by_user_by_latency.frm new file mode 100644 index 0000000..753e36e --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_by_user_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is not null and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +md5=c1f2d1207467817a5938ea4cd03c4e55 +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365946848 +create-version=2 +source=SELECT IF(user IS NULL, \'background\', user) AS user,\n event_name AS event,\n count_star AS total,\n sum_timer_wait AS total_latency,\n avg_timer_wait AS avg_latency,\n max_timer_wait AS max_latency\n FROM performance_schema.events_waits_summary_by_user_by_event_name\n WHERE event_name != \'idle\'\n AND user IS NOT NULL\n AND sum_timer_wait > 0\n ORDER BY user, sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`) AS `user`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` AS `event`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_waits_summary_by_user_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_by_user_by_event_name` where `performance_schema`.`events_waits_summary_by_user_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is not null and `performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` > 0 order by if(`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER` is null,\'background\',`performance_schema`.`events_waits_summary_by_user_by_event_name`.`USER`),`performance_schema`.`events_waits_summary_by_user_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_global_by_latency.frm b/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_global_by_latency.frm new file mode 100644 index 0000000..de722b4 --- /dev/null +++ b/src/main/docker-mariadb/mariadb_data/sys/x@0024waits_global_by_latency.frm @@ -0,0 +1,16 @@ +TYPE=VIEW +query=select `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` AS `events`,`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_global_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 order by `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` desc +md5=63c324ddff171469768e3ef704df8c1e +updatable=1 +algorithm=1 +definer_user=mariadb.sys +definer_host=localhost +suid=0 +with_check_option=0 +timestamp=0001747298365984107 +create-version=2 +source=SELECT event_name AS event,\n count_star AS total,\n sum_timer_wait AS total_latency,\n avg_timer_wait AS avg_latency,\n max_timer_wait AS max_latency\n FROM performance_schema.events_waits_summary_global_by_event_name\n WHERE event_name != \'idle\'\n AND sum_timer_wait > 0\n ORDER BY sum_timer_wait DESC; +client_cs_name=utf8mb3 +connection_cl_name=utf8mb3_general_ci +view_body_utf8=select `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` AS `events`,`performance_schema`.`events_waits_summary_global_by_event_name`.`COUNT_STAR` AS `total`,`performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` AS `total_latency`,`performance_schema`.`events_waits_summary_global_by_event_name`.`AVG_TIMER_WAIT` AS `avg_latency`,`performance_schema`.`events_waits_summary_global_by_event_name`.`MAX_TIMER_WAIT` AS `max_latency` from `performance_schema`.`events_waits_summary_global_by_event_name` where `performance_schema`.`events_waits_summary_global_by_event_name`.`EVENT_NAME` <> \'idle\' and `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` > 0 order by `performance_schema`.`events_waits_summary_global_by_event_name`.`SUM_TIMER_WAIT` desc +mariadb-version=110702 diff --git a/src/main/docker-mariadb/mariadb_data/undo001 b/src/main/docker-mariadb/mariadb_data/undo001 new file mode 100644 index 0000000..e114488 Binary files /dev/null and b/src/main/docker-mariadb/mariadb_data/undo001 differ diff --git a/src/main/docker-mariadb/mariadb_data/undo002 b/src/main/docker-mariadb/mariadb_data/undo002 new file mode 100644 index 0000000..3fc7748 Binary files /dev/null and b/src/main/docker-mariadb/mariadb_data/undo002 differ diff --git a/src/main/docker-mariadb/mariadb_data/undo003 b/src/main/docker-mariadb/mariadb_data/undo003 new file mode 100644 index 0000000..0a2017f Binary files /dev/null and b/src/main/docker-mariadb/mariadb_data/undo003 differ diff --git a/src/main/docker-nginx/Readme b/src/main/docker-nginx/Readme new file mode 100644 index 0000000..4f71ec3 --- /dev/null +++ b/src/main/docker-nginx/Readme @@ -0,0 +1,65 @@ +# ###################################################### # +# NGINX & Certbot # +# # +# This is a solution for a reverse-proxy configuration # +# and also letsencript certificate create and renew # +# # +# NGINX - Will act as a reverse proxy and SSL # +# Certbot - Will manage certificate's with Let's encript # +# ###################################################### # + +Project Structure: +================== + +docker-nginx/ +├── docker-compose.yml +├── nginx/ +│ ├── Dockerfile +│ └── default.conf +│ └── default.conf.FULL_HTTPS # The full NGINX configuration. With SSL support +│ └── default.conf.INITIAL_HTTP_ONLY # Minimal for Certbot firsttime certificate request. HTTP only. +├── certbot/ +│ ├── conf/ # Mounted volume for certs (empty at first) +│ └── www/ # Mounted volume for webroot challenge (empty at first) + + +NOTE: The first certificate needs a special environment. Thats why we need 2 distinct NGINX conf + + +Build & Run: +============ + +For a full startup: + +#1 copy&rename the file: nginx/default.conf.INITIAL_HTTP_ONLY to nginx/default.conf +#2 > docker-compose up --build +#3 copy&rename the file: nginx/default.conf.FULL_HTTPS to nginx/default.conf +#4 > docker-compose restart nginx + + +Certificate management +====================== + + 1. Self-Signed + + No need for Certbot. Comment docker-composer.yml configuration for certbot: + Generate or Copy the self-signed certificate files (*.crt & *.key) to \certs + Make sure docker-composer.yml maps the volume ./nginx/certs:/etc/nginx/certs + Make sure the nginx\default.conf uses the /etc/nginx/certs. + + 2. First Let's Encript certificate + + This is a very special situation. NGINX must be up & running to respond to Let's Encript challenge (/.well-known/acme-challenge/) + BUT, if we have HTTPS configured, NGINX will fail (no certificate, obviously). + Comment all server's in nginx\default.conf, keep the only the server for HTTP and /.well-known/acme-challenge/ + Uncomment docker-composer.yml certbot: configuration, including entrypoint. + Then, startup docker :: docker-compose up --build. Wait for everything to finish. A new certificate was requested, copied and configured into NGINX. + Stop. Uncomment all servers's in nginx\default.conf. Comment the docker-composer.yml certbot.entrypoint configuration. + + 3. Automate renoval of Let's Encript certificate + + #1 renew the certificate + > docker-compose run certbot renew --webroot --webroot-path=/var/www/certbot + #2 reload NGINX + > docker-compose exec nginx nginx -s reload + diff --git a/src/main/docker-nginx/docker-compose.yml b/src/main/docker-nginx/docker-compose.yml new file mode 100644 index 0000000..a4e3356 --- /dev/null +++ b/src/main/docker-nginx/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + nginx: + build: ./nginx + container_name: nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + - ./nginx/certs:/etc/nginx/certs + - ./certbot/www:/var/www/certbot + - ./certbot/conf:/etc/letsencrypt + restart: unless-stopped + + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - ./certbot/www:/var/www/certbot + - ./certbot/conf:/etc/letsencrypt + # entrypoint: > + # sh -c "certbot certonly --webroot --webroot-path=/var/www/certbot + # --email omguerreiro@gmail.com --agree-tos --no-eff-email + # -d innova.oguerreiro.com" \ No newline at end of file diff --git a/src/main/docker-nginx/nginx/Dockerfile b/src/main/docker-nginx/nginx/Dockerfile new file mode 100644 index 0000000..b7aa66f --- /dev/null +++ b/src/main/docker-nginx/nginx/Dockerfile @@ -0,0 +1,6 @@ +# Dockerfile + +FROM nginx:latest + +# Copy config file +COPY ./default.conf /etc/nginx/conf.d/ diff --git a/src/main/docker-nginx/nginx/certs/resilient_dev.crt b/src/main/docker-nginx/nginx/certs/resilient_dev.crt new file mode 100644 index 0000000..559e295 --- /dev/null +++ b/src/main/docker-nginx/nginx/certs/resilient_dev.crt @@ -0,0 +1,26 @@ +Bag Attributes + friendlyName: innova-ssl-cert + localKeyID: 54 69 6D 65 20 31 37 34 36 34 35 33 36 36 31 33 39 33 +subject=C = PT, ST = Lisbon, L = Lisbon, O = NOVA, OU = Dev, CN = localhost +issuer=C = PT, ST = Lisbon, L = Lisbon, O = NOVA, OU = Dev, CN = localhost +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIIDrjgAqiJU9cwDQYJKoZIhvcNAQELBQAwYDELMAkGA1UE +BhMCUFQxDzANBgNVBAgTBkxpc2JvbjEPMA0GA1UEBxMGTGlzYm9uMQ0wCwYDVQQK +EwROT1ZBMQwwCgYDVQQLEwNEZXYxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0yNTA1 +MDUxNDAxMDFaFw0zNTA1MDMxNDAxMDFaMGAxCzAJBgNVBAYTAlBUMQ8wDQYDVQQI +EwZMaXNib24xDzANBgNVBAcTBkxpc2JvbjENMAsGA1UEChMETk9WQTEMMAoGA1UE +CxMDRGV2MRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCuBBTd/KrlyBw+DHlcelSn8UpQp7IJttyME6JhhNx9i5bRzMOT +vlTVyyK3DlEjUxiXxcd31K1GWPSO57/kpnOP1ors02dofwIfwIy2Mp+bDB9gaydY +zVtNy0gqFKP0zaW4zxQFI77RsrGO7Uhly4zcqY+G3haQq/7i3pWonFusi8llYLAk +1QPTGMiCwWIssS88ouvV9S2SCbsuMdHO+sERdY8179wdlvlQ4mgvzwBls1U/upwE +MGUh+Mt0ZltOKJSP04uscq9JTIlCWekHfzI1yoF1LSINYifOdW5qNMGICbvSbCPr +DpIR8PSZ+SR9BedELcJOHv2LtA9eAkZCxrc5AgMBAAGjITAfMB0GA1UdDgQWBBRf +OowF84st9IoDkH9LQZhSI65EwTANBgkqhkiG9w0BAQsFAAOCAQEAaaQ5JDDld+eO +f0y0AodqfsY9ycZ10RGLYRHN/hXOrQnLQNHaWQOz3ElSrF7OArCC2lw+agVW5wiy +L5WAmh6Kdb5wcgmxYbNjJ4eEEWR4MzFMq8O3vgdcM2940I5r3PMF3HKUFq6NBivh +iJlV8vub+a7hdR7/niXnlbHq1Yaf0MpKQAQoRiLKYD1LMXo3h41yo9ocXGNoNN2d +VWFEXoaFXMN3C6A0keEASHoCbZzNDpoRk8yZMBLmdS0nxCY5xUWIjwg4FCGV0Wcu +lmjzQRiBjy132V4kjV2HvFJqPgyLgc9fac7qp7ZqWwkuUXN/slhPADCuHwR+n5NN +4pM/AI4sMg== +-----END CERTIFICATE----- diff --git a/src/main/docker-nginx/nginx/certs/resilient_dev.key b/src/main/docker-nginx/nginx/certs/resilient_dev.key new file mode 100644 index 0000000..36345b8 --- /dev/null +++ b/src/main/docker-nginx/nginx/certs/resilient_dev.key @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: innova-ssl-cert + localKeyID: 54 69 6D 65 20 31 37 34 36 34 35 33 36 36 31 33 39 33 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCuBBTd/KrlyBw+ +DHlcelSn8UpQp7IJttyME6JhhNx9i5bRzMOTvlTVyyK3DlEjUxiXxcd31K1GWPSO +57/kpnOP1ors02dofwIfwIy2Mp+bDB9gaydYzVtNy0gqFKP0zaW4zxQFI77RsrGO +7Uhly4zcqY+G3haQq/7i3pWonFusi8llYLAk1QPTGMiCwWIssS88ouvV9S2SCbsu +MdHO+sERdY8179wdlvlQ4mgvzwBls1U/upwEMGUh+Mt0ZltOKJSP04uscq9JTIlC +WekHfzI1yoF1LSINYifOdW5qNMGICbvSbCPrDpIR8PSZ+SR9BedELcJOHv2LtA9e +AkZCxrc5AgMBAAECggEAAInRMbWeJrtVgQijWHqlZ+aacwDtz1NiP8mwwyn4z6ee +phQC1+JrG0U3XIceD6Sqaw/I0/FTvue4C/giExhDu8JvaBeoVn2sGUKMfOTPsxhY +wYDbXI44FdfG56BsOG3pcRas6m4noTjzDSzlQDFexHD+2W029ygdEAEdx+mB7Bj6 +Tm5LbBlwKoIqOmrJ9kHUyi63RQj135m3yrHCGzsecEU+HJDPGaYRPz89068Ja5la +3uh6yhm5SFOn/4Oj4vx1/dABtCvIsDXoaVJZVprVqPVX1AmnBSedlbKFkNkqrz2+ +D03aTdFa1XGghqVbguPpO6qXTh6JKhlt9QONM2q44QKBgQDVj7p1megCiojC49cf +pYesbT2/pYFLX6jPQZCU6P0Ty0zLX6UEE5FEe9JkoMfA+6ytf82zweJlF2U/JWXs +J9+AQU22rACMSLTYNzRxzjlcEUbm133HHjoK77zN6T1rcPLbf51hkKRyIqxH2DHR +qoySz1I8ueRkd8PfuWocgyrsEQKBgQDQmJl43IrQvUEft4PSue3Kw2SaCabSBXCG +ZIRy49ACArHpAuaSBCD8FwzDkv0D5gxgFBRuUC1Ru70e4AfRyp73jVdo0mABjJ++ +pIs4yAxeIz9gDcS/PeZ4BCZ1vPap2zTMsZ2yCglXbeWzCocLzTStDx8b4hT96xl3 +XoHLuMLgqQKBgGm5/yiLakYfyXcajvzW2SUu69v+FtV25T6+CdL+yZPMGReyISK0 +gT1zQ4SX6GyM7D70v6SXfvpnK7OoUx4n20lGiy+9VYQd1pIYOnmBC/qdfwl3c4hp +WXjimQkjyElxoHm0YvjTP+ZVbg8fZAKVuYRQ6TixGvX2KN2QkIzPDyfRAoGAbr+G +RBf7j4XbjKZXU8spSAzjXbEgI8OFkaTOeV0gc+DQamEC36VXieAHA9MHiElcaqpe +io8A8LMXXswc+rJ0IgSl0t5W4JuzuHkN/bCgeF6IaEwHGG4Z7cBuVvZjk5zxdHgc +vIj9mrCbUqqVNpvVishLgPdQo9ttYuYHTY0j0jkCgYEAvqddASppkAb4hYHwA03C +PAEhErq13hb1+RdIOmCKwIvDrnti9MKTpCDQ1LXa2gIWy4DxiOWUo3Ryvwr/Dov7 +1de1+1jNmbIq5bzaGzlbYoWeBczpil5DXhVH5qVrWgcmMykUCGPEO6PCyj/V8EMg +z9Yfk3XR9DNMfe6ry2IOqM4= +-----END PRIVATE KEY----- diff --git a/src/main/docker-nginx/nginx/default.conf b/src/main/docker-nginx/nginx/default.conf new file mode 100644 index 0000000..479aeb1 --- /dev/null +++ b/src/main/docker-nginx/nginx/default.conf @@ -0,0 +1,109 @@ +# NGINX default.conf file +# +# Acts has a reverse proxy. +# The strategy is to keep Angular and SpringBoot App no HTTP only. +# The NGINX will hide the APP, that will support only standard HTTP or HTTPS. +# The HTTPS will be configured only in NGINX. Making it more secure but also +# simplify the certificat configuration process. +# +# General idea: +# *1 User browser : http://resilient.localhost +# *2 NGINX redirects to : https://resilient.localhost +# *3 NGINX redirects to : http://host.docker.internal:4200 +# +# The hostname : host.docker.internal, is the equivalent to localhost in the machine +# running the docker + +# HTTP APP URL Configuration +# Listens to http://resilient.localhost +# if its a Let's encrypt challenge requests, points to certbot. +# else redirects to https://resilient.localhost +server { + listen 80; + listen [::]:80; + server_name resilient.localhost; + + # Listen to Let's Encrypt challenge requests. DON'T redirect to HTTPS + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other HTTP requests to HTTPS. + # NOTE: To allow access to HTTP, comment this. Uncomment the location/ + return 301 https://$host$request_uri; + + # Uncomment this, to allow HTTP requests + # location / { + # proxy_pass http://host.docker.internal:4200; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } +} + +# HTTPS APP URL Configuration +# Listens to https://resilient.localhost and proxies to http://localhost:8081 +# Also defines SSL certificates. +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name resilient.localhost; + + #access_log /var/log/nginx/host.access.log main; + + ssl_certificate /etc/nginx/certs/resilient_dev.crt; + ssl_certificate_key /etc/nginx/certs/resilient_dev.key; + + # 👇 Use this for letsencrypt generated certificate + # ssl_certificate /etc/letsencrypt/live/innova.oguerreiro.com/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/innova.oguerreiro.com/privkey.pem; + + location / { + proxy_pass http://host.docker.internal:4200; + + # 👇 This skips cert verification, REQUIRED for self-signed certs + # Comment if it's NOT a self-signed cert + proxy_ssl_verify off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} + +# MOCK-IDP URL Configuration +# Listens to http://mock-idp.localhost and proxies to http://localhost:3000 +server { + listen 80; + listen [::]:80; + server_name mock-idp.localhost; + + #access_log /var/log/nginx/host.access.log main; + + location / { + proxy_pass http://host.docker.internal:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/src/main/docker/app.yml b/src/main/docker/app.yml new file mode 100644 index 0000000..f190612 --- /dev/null +++ b/src/main/docker/app.yml @@ -0,0 +1,29 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: resilient +services: + app: + image: resilient + environment: + - _JAVA_OPTIONS=-Xmx512m -Xms256m + - SPRING_PROFILES_ACTIVE=prod,api-docs + - MANAGEMENT_PROMETHEUS_METRICS_EXPORT_ENABLED=true + - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true + - SPRING_LIQUIBASE_URL=jdbc:mysql://mysql:3306/resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true + ports: + - 127.0.0.1:8081:8081 + healthcheck: + test: + - CMD + - curl + - -f + - http://localhost:8081/management/health + interval: 5s + timeout: 5s + retries: 40 + depends_on: + mysql: + condition: service_healthy + mysql: + extends: + file: ./mysql.yml + service: mysql diff --git a/src/main/docker/config/mysql/my.cnf b/src/main/docker/config/mysql/my.cnf new file mode 100644 index 0000000..7ce91a8 --- /dev/null +++ b/src/main/docker/config/mysql/my.cnf @@ -0,0 +1,81 @@ +# For advice on how to change settings please see +# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html +[mysqld] +user = mysql +datadir = /var/lib/mysql +port = 3306 +#socket = /tmp/mysql.sock +skip-external-locking +key_buffer_size = 16K +max_allowed_packet = 1M +table_open_cache = 4 +sort_buffer_size = 64K +read_buffer_size = 256K +read_rnd_buffer_size = 256K +net_buffer_length = 2K +skip-name-resolve + +# Don't listen on a TCP/IP port at all. This can be a security enhancement, +# if all processes that need to connect to mysqld run on the same host. +# All interaction with mysqld must be made via Unix sockets or named pipes. +# Note that using this option without enabling named pipes on Windows +# (using the "enable-named-pipe" option) will render mysqld useless! +# +#skip-networking +#server-id = 1 + +# Uncomment the following if you want to log updates +#log-bin=mysql-bin + +# binary logging format - mixed recommended +#binlog_format=mixed + +# Causes updates to non-transactional engines using statement format to be +# written directly to binary log. Before using this option make sure that +# there are no dependencies between transactional and non-transactional +# tables such as in the statement INSERT INTO t_myisam SELECT * FROM +# t_innodb; otherwise, slaves may diverge from the master. +#binlog_direct_non_transactional_updates=TRUE + +# Uncomment the following if you are using InnoDB tables +innodb_data_file_path = ibdata1:10M:autoextend +# You can set .._buffer_pool_size up to 50 - 80 % +# of RAM but beware of setting memory usage too high +innodb_buffer_pool_size = 16M +#innodb_additional_mem_pool_size = 2M +# Set .._log_file_size to 25 % of buffer pool size +innodb_log_file_size = 5M +innodb_log_buffer_size = 8M +innodb_flush_log_at_trx_commit = 1 +innodb_lock_wait_timeout = 50 + +symbolic-links=0 +innodb_buffer_pool_size=5M +innodb_log_buffer_size=256K +max_connections=20 +key_buffer_size=8 +thread_cache_size=0 +host_cache_size=0 +innodb_ft_cache_size=1600000 +innodb_ft_total_cache_size=32000000 +#### These optimize the memory use of MySQL +#### http://www.tocker.ca/2014/03/10/configuring-mysql-to-use-minimal-memory.html + +# per thread or per operation settings +thread_stack=131072 +sort_buffer_size=32K +read_buffer_size=8200 +read_rnd_buffer_size=8200 +max_heap_table_size=16K +tmp_table_size=1K +bulk_insert_buffer_size=0 +join_buffer_size=128 +net_buffer_length=1K +innodb_sort_buffer_size=64K + +#settings that relate to the binary log (if enabled) +binlog_cache_size=4K +binlog_stmt_cache_size=4K + +performance_schema = off +character-set-server = utf8mb4 diff --git a/src/main/docker/grafana/provisioning/dashboards/JVM.json b/src/main/docker/grafana/provisioning/dashboards/JVM.json new file mode 100644 index 0000000..5104abc --- /dev/null +++ b/src/main/docker/grafana/provisioning/dashboards/JVM.json @@ -0,0 +1,3778 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "limit": 100, + "name": "Annotations & Alerts", + "showIn": 0, + "type": "dashboard" + }, + { + "datasource": "Prometheus", + "enable": true, + "expr": "resets(process_uptime_seconds{application=\"$application\", instance=\"$instance\"}[1m]) > 0", + "iconColor": "rgba(255, 96, 96, 1)", + "name": "Restart Detection", + "showIn": 0, + "step": "1m", + "tagKeys": "restart-tag", + "textFormat": "uptime reset", + "titleFormat": "Restart" + } + ] + }, + "description": "Dashboard for Micrometer instrumented applications (Java, Spring Boot, Micronaut)", + "editable": true, + "gnetId": 4701, + "graphTooltip": 1, + "iteration": 1553765841423, + "links": [], + "panels": [ + { + "content": "\n# Acknowledgments\n\nThank you to [Michael Weirauch](https://twitter.com/emwexx) for creating this dashboard: see original JVM (Micrometer) dashboard at [https://grafana.com/dashboards/4701](https://grafana.com/dashboards/4701)\n\n\n\n", + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 141, + "links": [], + "mode": "markdown", + "timeFrom": null, + "timeShift": null, + "title": "Acknowledgments", + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 125, + "panels": [], + "repeat": null, + "title": "Quick Facts", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "Prometheus", + "decimals": 1, + "editable": true, + "error": false, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "height": "", + "id": 63, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "process_uptime_seconds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "Prometheus", + "decimals": null, + "editable": true, + "error": false, + "format": "dateTimeAsIso", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "height": "", + "id": 92, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "process_start_time_seconds{application=\"$application\", instance=\"$instance\"}*1000", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Start time", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 65, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 75, + "interval": null, + "links": [], + "mappingType": 2, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + }, + { + "from": "-99999999999999999999999999999999", + "text": "N/A", + "to": "0" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Non-Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + }, + { + "op": "=", + "text": "x", + "value": "" + } + ], + "valueName": "current" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 126, + "panels": [], + "repeat": null, + "title": "I/O Overview", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 111, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "HTTP": "#890f02", + "HTTP - 5xx": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 112, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status=~\"5..\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP - 5xx", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Errors", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 113, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_sum{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))/sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - AVG", + "refId": "A" + }, + { + "expr": "max(http_server_requests_seconds_max{application=\"$application\", instance=\"$instance\", status!~\"5..\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - MAX", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 127, + "panels": [], + "repeat": null, + "title": "JVM Memory", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 24, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 25, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Non-Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 26, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + }, + { + "expr": "process_memory_vss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": true, + "intervalFactor": 2, + "legendFormat": "vss", + "metric": "", + "refId": "D", + "step": 2400 + }, + { + "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "rss", + "refId": "E", + "step": 2400 + }, + { + "expr": "process_memory_pss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "pss", + "refId": "F", + "step": 2400 + }, + { + "expr": "process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swap", + "refId": "G", + "step": 2400 + }, + { + "expr": "process_memory_swappss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swappss", + "refId": "H", + "step": 2400 + }, + { + "expr": "process_memory_pss_bytes{application=\"$application\", instance=\"$instance\"} + process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "phys (pss+swap)", + "refId": "I", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Total", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 128, + "panels": [], + "repeat": null, + "title": "JVM Misc", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 24 + }, + "id": 106, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "system", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process", + "refId": "B" + }, + { + "expr": "avg_over_time(process_cpu_usage{application=\"$application\", instance=\"$instance\"}[1h])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process-1h", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "CPU", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 1, + "format": "percentunit", + "label": "", + "logBase": 1, + "max": "1", + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 24 + }, + "id": 93, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_load_average_1m{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "system-1m", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 2, + "refId": "B" + }, + { + "expr": "system_cpu_count{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "cpu", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Load", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 1, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 24 + }, + "id": 32, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_live{application=\"$application\", instance=\"$instance\"} or jvm_threads_live_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "live", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_threads_daemon{application=\"$application\", instance=\"$instance\"} or jvm_threads_daemon_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "daemon", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "expr": "jvm_threads_peak{application=\"$application\", instance=\"$instance\"} or jvm_threads_peak_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "peak", + "refId": "C", + "step": 2400 + }, + { + "expr": "process_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "process", + "refId": "D", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Threads", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "blocked": "#bf1b00", + "new": "#fce2de", + "runnable": "#7eb26d", + "terminated": "#511749", + "timed-waiting": "#c15c17", + "waiting": "#eab839" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 24 + }, + "id": 124, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_states_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{state}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Thread States", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "debug": "#1F78C1", + "error": "#BF1B00", + "info": "#508642", + "trace": "#6ED0E0", + "warn": "#EAB839" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 18, + "x": 0, + "y": 31 + }, + "height": "", + "id": 91, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "error", + "yaxis": 1 + }, + { + "alias": "warn", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "increase(logback_events_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{level}}", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Log Events (1m)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 31 + }, + "id": 61, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_open_fds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "open", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_max_fds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "expr": "process_files_open{application=\"$application\", instance=\"$instance\"} or process_files_open_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "open", + "refId": "C" + }, + { + "expr": "process_files_max{application=\"$application\", instance=\"$instance\"} or process_files_max_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "File Descriptors", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 10, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 129, + "panels": [], + "repeat": "persistence_counts", + "title": "JVM Memory Pools (Heap)", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 39 + }, + "id": 3, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_heap", + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Eden Space", + "value": "PS Eden Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 39 + }, + "id": 134, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 3, + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Old Gen", + "value": "PS Old Gen" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 39 + }, + "id": 135, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 3, + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Survivor Space", + "value": "PS Survivor Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 46 + }, + "id": 130, + "panels": [], + "repeat": null, + "title": "JVM Memory Pools (Non-Heap)", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 47 + }, + "id": 78, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_nonheap", + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Metaspace", + "value": "Metaspace" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 47 + }, + "id": 136, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 78, + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Compressed Class Space", + "value": "Compressed Class Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 47 + }, + "id": 137, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 78, + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Code Cache", + "value": "Code Cache" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 54 + }, + "id": 131, + "panels": [], + "repeat": null, + "title": "Garbage Collection", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 55 + }, + "id": 98, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{action}} ({{cause}})", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Collections", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 55 + }, + "id": 101, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_seconds_sum{application=\"$application\", instance=\"$instance\"}[1m])/rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "avg {{action}} ({{cause}})", + "refId": "A" + }, + { + "expr": "jvm_gc_pause_seconds_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "max {{action}} ({{cause}})", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Pause Durations", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 55 + }, + "id": 99, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_memory_allocated_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "allocated", + "refId": "A" + }, + { + "expr": "rate(jvm_gc_memory_promoted_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "promoted", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Allocated/Promoted", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 62 + }, + "id": 132, + "panels": [], + "repeat": null, + "title": "Classloading", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 63 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_classes_loaded{application=\"$application\", instance=\"$instance\"} or jvm_classes_loaded_classes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "loaded", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Classes loaded", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 63 + }, + "id": 38, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "delta(jvm_classes_loaded{application=\"$application\",instance=\"$instance\"}[5m]) or delta(jvm_classes_loaded_classes{application=\"$application\",instance=\"$instance\"}[5m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "delta", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Class delta (5m)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["ops", "short"], + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 133, + "panels": [], + "repeat": null, + "title": "Buffer Pools", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 71 + }, + "id": 33, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 71 + }, + "id": 83, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_count{application=\"$application\", instance=\"$instance\", id=\"direct\"} or jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 71 + }, + "id": 85, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 71 + }, + "id": 84, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_count{application=\"$application\", instance=\"$instance\", id=\"mapped\"} or jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "10s", + "schemaVersion": 18, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "text": "test", + "value": "test" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Application", + "multi": false, + "name": "application", + "options": [], + "query": "label_values(application)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "localhost:8080", + "value": "localhost:8080" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Instance", + "multi": false, + "multiFormat": "glob", + "name": "instance", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\"}, instance)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_heap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Non-Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_nonheap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 2, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "browser", + "title": "JVM (Micrometer)", + "uid": "Ud1CFe3iz", + "version": 1 +} diff --git a/src/main/docker/grafana/provisioning/dashboards/dashboard.yml b/src/main/docker/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000..4817a83 --- /dev/null +++ b/src/main/docker/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'Prometheus' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards diff --git a/src/main/docker/grafana/provisioning/datasources/datasource.yml b/src/main/docker/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 0000000..57b2bb3 --- /dev/null +++ b/src/main/docker/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,50 @@ +apiVersion: 1 + +# list of datasources that should be deleted from the database +deleteDatasources: + - name: Prometheus + orgId: 1 + +# list of datasources to insert/update depending +# whats available in the database +datasources: + # name of the datasource. Required + - name: Prometheus + # datasource type. Required + type: prometheus + # access mode. direct or proxy. Required + access: proxy + # org id. will default to orgId 1 if not specified + orgId: 1 + # url + # On MacOS, replace localhost by host.docker.internal + url: http://localhost:9090 + # database password, if used + password: + # database user, if used + user: + # database name, if used + database: + # enable/disable basic auth + basicAuth: false + # basic auth username + basicAuthUser: admin + # basic auth password + basicAuthPassword: admin + # enable/disable with credentials headers + withCredentials: + # mark as default datasource. Max one per org + isDefault: true + # fields that will be converted to json and stored in json_data + jsonData: + graphiteVersion: '1.1' + tlsAuth: false + tlsAuthWithCACert: false + # json object of data that will be encrypted. + secureJsonData: + tlsCACert: '...' + tlsClientCert: '...' + tlsClientKey: '...' + version: 1 + # allow users to edit datasources from the UI. + editable: true diff --git a/src/main/docker/jhipster-control-center.yml b/src/main/docker/jhipster-control-center.yml new file mode 100644 index 0000000..840edd8 --- /dev/null +++ b/src/main/docker/jhipster-control-center.yml @@ -0,0 +1,48 @@ +## How to use JHCC docker compose +# To allow JHCC to reach JHipster application from a docker container note that we set the host as host.docker.internal +# To reach the application from a browser, you need to add '127.0.0.1 host.docker.internal' to your hosts file. +### Discovery mode +# JHCC support 3 kinds of discovery mode: Consul, Eureka and static +# In order to use one, please set SPRING_PROFILES_ACTIVE to one (and only one) of this values: consul,eureka,static +### Discovery properties +# According to the discovery mode choose as Spring profile, you have to set the right properties +# please note that current properties are set to run JHCC with default values, personalize them if needed +# and remove those from other modes. You can only have one mode active. +#### Eureka +# - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://admin:admin@host.docker.internal:8761/eureka/ +#### Consul +# - SPRING_CLOUD_CONSUL_HOST=host.docker.internal +# - SPRING_CLOUD_CONSUL_PORT=8500 +#### Static +# Add instances to "MyApp" +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_0_URI=http://host.docker.internal:8081 +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_1_URI=http://host.docker.internal:8082 +# Or add a new application named MyNewApp +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYNEWAPP_0_URI=http://host.docker.internal:8080 +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production + +#### IMPORTANT +# If you choose Consul or Eureka mode: +# Do not forget to remove the prefix "127.0.0.1" in front of their port in order to expose them. +# This is required because JHCC need to communicate with Consul or Eureka. +# - In Consul mode, the ports are in the consul.yml file. +# - In Eureka mode, the ports are in the jhipster-registry.yml file. + +name: resilient +services: + jhipster-control-center: + image: 'jhipster/jhipster-control-center:v0.5.0' + command: + - /bin/sh + - -c + # Patch /etc/hosts to support resolving host.docker.internal to the internal IP address used by the host in all OSes + - echo "`ip route | grep default | cut -d ' ' -f3` host.docker.internal" | tee -a /etc/hosts > /dev/null && java -jar /jhipster-control-center.jar + environment: + - _JAVA_OPTIONS=-Xmx512m -Xms256m + - SPRING_PROFILES_ACTIVE=prod,api-docs,static + - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_RESILIENT_0_URI=http://host.docker.internal:8081 + - LOGGING_FILE_NAME=/tmp/jhipster-control-center.log + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:7419:7419 diff --git a/src/main/docker/jib/entrypoint.sh b/src/main/docker/jib/entrypoint.sh new file mode 100644 index 0000000..e40af92 --- /dev/null +++ b/src/main/docker/jib/entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +echo "The application will start in ${JHIPSTER_SLEEP}s..." && sleep ${JHIPSTER_SLEEP} + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [[ ${!var:-} && ${!fileVar:-} ]]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [[ ${!var:-} ]]; then + val="${!var}" + elif [[ ${!fileVar:-} ]]; then + val="$(< "${!fileVar}")" + fi + + if [[ -n $val ]]; then + export "$var"="$val" + fi + + unset "$fileVar" +} + +file_env 'SPRING_DATASOURCE_URL' +file_env 'SPRING_DATASOURCE_USERNAME' +file_env 'SPRING_DATASOURCE_PASSWORD' +file_env 'SPRING_LIQUIBASE_URL' +file_env 'SPRING_LIQUIBASE_USER' +file_env 'SPRING_LIQUIBASE_PASSWORD' +file_env 'JHIPSTER_REGISTRY_PASSWORD' + +exec java ${JAVA_OPTS} -noverify -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -cp /app/resources/:/app/classes/:/app/libs/* "com.oguerreiro.resilient.ResilientApp" "$@" diff --git a/src/main/docker/monitoring.yml b/src/main/docker/monitoring.yml new file mode 100644 index 0000000..0d39f3c --- /dev/null +++ b/src/main/docker/monitoring.yml @@ -0,0 +1,31 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: resilient +services: + prometheus: + image: prom/prometheus:v2.52.0 + volumes: + - ./prometheus/:/etc/prometheus/ + command: + - '--config.file=/etc/prometheus/prometheus.yml' + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:9090:9090 + # On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and + # grafana/provisioning/datasources/datasource.yml + network_mode: 'host' # to test locally running service + grafana: + image: grafana/grafana:11.0.0 + volumes: + - ./grafana/provisioning/:/etc/grafana/provisioning/ + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + - GF_INSTALL_PLUGINS=grafana-piechart-panel + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:3000:3000 + # On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and + # grafana/provisioning/datasources/datasource.yml + network_mode: 'host' # to test locally running service diff --git a/src/main/docker/mysql.yml b/src/main/docker/mysql.yml new file mode 100644 index 0000000..6ae915f --- /dev/null +++ b/src/main/docker/mysql.yml @@ -0,0 +1,21 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: resilient +services: + mysql: + image: mysql:8.4.0 + volumes: + - ./config/mysql:/etc/mysql/conf.d + # - ~/volumes/jhipster/resilient/mysql/:/var/lib/mysql/ + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=resilient + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:3306:3306 + command: mysqld --lower_case_table_names=1 --character_set_server=utf8mb4 --explicit_defaults_for_timestamp + healthcheck: + test: ['CMD', 'mysql', '-e', 'SHOW DATABASES;'] + interval: 5s + timeout: 5s + retries: 10 diff --git a/src/main/docker/prometheus/prometheus.yml b/src/main/docker/prometheus/prometheus.yml new file mode 100644 index 0000000..4c6c1c9 --- /dev/null +++ b/src/main/docker/prometheus/prometheus.yml @@ -0,0 +1,31 @@ +# Sample global config for monitoring JHipster applications +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + evaluation_interval: 15s # By default, scrape targets every 15 seconds. + # scrape_timeout is set to the global default (10s). + + # Attach these labels to any time series or alerts when communicating with + # external systems (federation, remote storage, Alertmanager). + external_labels: + monitor: 'jhipster' + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + + # scheme defaults to 'http' enable https in case your application is server via https + #scheme: https + # basic auth is not needed by default. See https://www.jhipster.tech/monitoring/#configuring-metrics-forwarding for details + #basic_auth: + # username: admin + # password: admin + metrics_path: /management/prometheus + static_configs: + - targets: + # On MacOS, replace localhost by host.docker.internal + - localhost:8081 diff --git a/src/main/docker/services.yml b/src/main/docker/services.yml new file mode 100644 index 0000000..681f64e --- /dev/null +++ b/src/main/docker/services.yml @@ -0,0 +1,7 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: resilient +services: + mysql: + extends: + file: ./mysql.yml + service: mysql diff --git a/src/main/docker/sonar.yml b/src/main/docker/sonar.yml new file mode 100644 index 0000000..b9bf44d --- /dev/null +++ b/src/main/docker/sonar.yml @@ -0,0 +1,15 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: resilient +services: + sonar: + container_name: sonarqube + image: sonarqube:10.5.1-community + # Forced authentication redirect for UI is turned off for out of the box experience while trying out SonarQube + # For real use cases delete SONAR_FORCEAUTHENTICATION variable or set SONAR_FORCEAUTHENTICATION=true + environment: + - SONAR_FORCEAUTHENTICATION=false + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:9001:9000 + - 127.0.0.1:9000:9000 diff --git a/src/main/java/com/oguerreiro/resilient/ApplicationWebXml.java b/src/main/java/com/oguerreiro/resilient/ApplicationWebXml.java new file mode 100644 index 0000000..da290a3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/ApplicationWebXml.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import tech.jhipster.config.DefaultProfileUtil; + +/** + * This is a helper Java class that provides an alternative to creating a {@code web.xml}. + * This will be invoked only when the application is deployed to a Servlet container like Tomcat, JBoss etc. + */ +public class ApplicationWebXml extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + // set a default to use when no profile is configured. + DefaultProfileUtil.addDefaultProfile(application.application()); + return application.sources(ResilientApp.class); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/GeneratedByJHipster.java b/src/main/java/com/oguerreiro/resilient/GeneratedByJHipster.java new file mode 100644 index 0000000..341f78c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/GeneratedByJHipster.java @@ -0,0 +1,13 @@ +package com.oguerreiro.resilient; + +import jakarta.annotation.Generated; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Generated(value = "JHipster", comments = "Generated by JHipster 8.5.0") +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE }) +public @interface GeneratedByJHipster { +} diff --git a/src/main/java/com/oguerreiro/resilient/ResilientApp.java b/src/main/java/com/oguerreiro/resilient/ResilientApp.java new file mode 100644 index 0000000..cdad349 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/ResilientApp.java @@ -0,0 +1,99 @@ +package com.oguerreiro.resilient; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.env.Environment; + +import com.oguerreiro.resilient.config.ApplicationProperties; +import com.oguerreiro.resilient.config.CRLFLogConverter; + +import jakarta.annotation.PostConstruct; +import tech.jhipster.config.DefaultProfileUtil; +import tech.jhipster.config.JHipsterConstants; + +@SpringBootApplication +@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class }) +public class ResilientApp { + + private static final Logger log = LoggerFactory.getLogger(ResilientApp.class); + + private final Environment env; + + public ResilientApp(Environment env) { + this.env = env; + } + + /** + * Initializes resilient. + *

+ * Spring profiles can be configured with a program argument --spring.profiles.active=your-active-profile + *

+ * You can find more information on how profiles work with JHipster on + * https://www.jhipster.tech/profiles/. + */ + @PostConstruct + public void initApplication() { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + && activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { + log.error("You have misconfigured your application! It should not run " + + "with both the 'dev' and 'prod' profiles at the same time."); + } + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + && activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_CLOUD)) { + log.error("You have misconfigured your application! It should not " + + "run with both the 'dev' and 'cloud' profiles at the same time."); + } + } + + /** + * Main method, used to run the application. + * + * @param args the command line arguments. + */ + public static void main(String[] args) { + // Add Bouncy Castle as a security provider. Without this, it throws the exception: + // java.security.NoSuchAlgorithmException: RIPEMD160 MessageDigest not available + // Security.addProvider(new BouncyCastleProvider()); + + SpringApplication app = new SpringApplication(ResilientApp.class); + DefaultProfileUtil.addDefaultProfile(app); + Environment env = app.run(args).getEnvironment(); + logApplicationStartup(env); + } + + private static void logApplicationStartup(Environment env) { + String protocol = Optional.ofNullable(env.getProperty("server.ssl.key-store")).map(key -> "https").orElse("http"); + String applicationName = env.getProperty("spring.application.name"); + String serverPort = env.getProperty("server.port"); + String contextPath = Optional.ofNullable(env.getProperty("server.servlet.context-path")).filter( + StringUtils::isNotBlank).orElse("/"); + String hostAddress = "localhost"; + try { + hostAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.warn("The host name could not be determined, using `localhost` as fallback"); + } + log.info(CRLFLogConverter.CRLF_SAFE_MARKER, """ + + ---------------------------------------------------------- + \tApplication '{}' is running! Access URLs: + \tLocal: \t\t{}://localhost:{}{} + \tExternal: \t{}://{}:{}{} + \tProfile(s): \t{} + ----------------------------------------------------------""", applicationName, protocol, serverPort, + contextPath, protocol, hostAddress, serverPort, contextPath, + env.getActiveProfiles().length == 0 ? env.getDefaultProfiles() : env.getActiveProfiles()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataTask.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataTask.java new file mode 100644 index 0000000..4251526 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataTask.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.activities.inputDataImporter; + +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.Task; +import com.oguerreiro.resilient.domain.InputDataUpload; + +public interface ClearInputDataTask extends Task> { + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataTaskImpl.java new file mode 100644 index 0000000..a05631c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataTaskImpl.java @@ -0,0 +1,33 @@ +package com.oguerreiro.resilient.activities.inputDataImporter; + +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.activity.AbstractTask; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.service.InputDataService; + +@Component +public class ClearInputDataTaskImpl extends + AbstractTask, ClearInputDataTask> implements ClearInputDataTask { + + private final InputDataService inputDataService; + + public ClearInputDataTaskImpl(InputDataService inputDataService) { + this.inputDataService = inputDataService; + } + + @Override + public GenericResult doTask(GenericGuide guide) { + InputDataUpload inputDataUpload = guide.getActivityDomain(); + + // Clear previously imported InputData. + this.inputDataService.deleteAllInputDataFromDatafileInNewTransaction(inputDataUpload.getOwner(), + inputDataUpload.getPeriod(), inputDataUpload.getType()); + + return new GenericResult(WorkResultEnum.COMPLETED); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataUploadLogTask.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataUploadLogTask.java new file mode 100644 index 0000000..ed48210 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataUploadLogTask.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.activities.inputDataImporter; + +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.Task; +import com.oguerreiro.resilient.domain.InputDataUpload; + +public interface ClearInputDataUploadLogTask extends Task> { + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataUploadLogTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataUploadLogTaskImpl.java new file mode 100644 index 0000000..567b34b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/ClearInputDataUploadLogTaskImpl.java @@ -0,0 +1,35 @@ +package com.oguerreiro.resilient.activities.inputDataImporter; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.activity.AbstractTask; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.service.InputDataUploadLogService; + +@Qualifier("myClearInputDataUploadLogTask") +@Component +public class ClearInputDataUploadLogTaskImpl + extends AbstractTask, ClearInputDataUploadLogTask> + implements ClearInputDataUploadLogTask { + + private final InputDataUploadLogService inputDataUploadLogService; + + public ClearInputDataUploadLogTaskImpl(InputDataUploadLogService inputDataUploadLogService) { + this.inputDataUploadLogService = inputDataUploadLogService; + } + + @Override + public GenericResult doTask(GenericGuide guide) { + InputDataUpload inputDataUpload = guide.getActivityDomain(); + + //Clear logs for InputdataUpload entity + inputDataUploadLogService.deleteAllByInputDataUploadWithNewTransaction(inputDataUpload); + + return new GenericResult(WorkResultEnum.COMPLETED); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterActivity.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterActivity.java new file mode 100644 index 0000000..55d2409 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterActivity.java @@ -0,0 +1,16 @@ +package com.oguerreiro.resilient.activities.inputDataImporter; + +import java.util.concurrent.CompletableFuture; + +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.enumeration.UploadStatus; + +public interface POIFileImporterActivity extends Activity> { + CompletableFuture doInternalActivityAsync(GenericGuide guide); + Integer countErrors(InputDataUpload inputDataUpload); + + InputDataUpload updateStateWithNewTransaction(InputDataUpload inputDataUpload, UploadStatus newUploadStatus); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterActivityImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterActivityImpl.java new file mode 100644 index 0000000..1a8c359 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterActivityImpl.java @@ -0,0 +1,228 @@ +package com.oguerreiro.resilient.activities.inputDataImporter; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.activities.inputDataImporter.importers.InputDataImporterTask; +import com.oguerreiro.resilient.activity.AbstractActivity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.TaskResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor; +import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.domain.enumeration.UploadStatus; +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.error.ImporterPeriodNotOpenedToReceiveDataRuntimeException; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.repository.InputDataUploadRepository; +import com.oguerreiro.resilient.repository.PeriodRepository; +import com.oguerreiro.resilient.security.SecurityUtils; + +import jakarta.persistence.EntityManager; + +@Component +public class POIFileImporterActivityImpl + extends AbstractActivity, POIFileImporterActivity> + implements POIFileImporterActivity { + private final Logger log = LoggerFactory.getLogger(POIFileImporterActivityImpl.class); + + private static final String ACTIVITY_KEY = "input-data-upload-import"; + + /* ----- TASKS ----- */ + private final ClearInputDataUploadLogTask clearInputDataUploadLogTask; + private final ClearInputDataTask clearInputDataTask; + private final List inputDataImporterTasks; + + /* ----- BEANS ----- */ + private final PeriodRepository periodRepository; + private final InputDataUploadLogRepository inputDataUploadLogRepository; + private final InputDataUploadRepository inputDataUploadRepository; + private final EntityManager entityManager; + private final ActivityProgressRegistry activityProgressRegistry; + + public POIFileImporterActivityImpl(ClearInputDataUploadLogTask clearInputDataUploadLogTask, + ClearInputDataTask clearInputDataTask, InputDataUploadLogRepository inputDataUploadLogRepository, + InputDataUploadRepository inputDataUploadRepository, EntityManager entityManager, + ActivityProgressRegistry activityProgressRegistry, List inputDataImporterTasks, + PeriodRepository periodRepository) { + this.clearInputDataUploadLogTask = clearInputDataUploadLogTask; + this.clearInputDataTask = clearInputDataTask; + this.inputDataUploadLogRepository = inputDataUploadLogRepository; + this.inputDataUploadRepository = inputDataUploadRepository; + this.entityManager = entityManager; + this.activityProgressRegistry = activityProgressRegistry; + this.inputDataImporterTasks = inputDataImporterTasks; + this.periodRepository = periodRepository; + } + + /* ----- SelectionStrategy properties ----- */ + @Override + public Class appliesToActivityDomainClass() { + return InputDataUpload.class; + } + + @Override + public List appliesToStates() { + return Arrays.asList(UploadStatus.UPLOADED.toString(), UploadStatus.ERROR.toString()); + } + + /* ----- Activity implementation ----- */ + @Override + public String getActivityKey() { + return ACTIVITY_KEY; + } + + @Transactional + @Override + public GenericResult doActivity(String activityDomainClass, Long activityDomainId) { + // Load Period + InputDataUpload inputDataUpload = inputDataUploadRepository.getReferenceById(activityDomainId); + + ActivityProgressDescriptor activityProgressDescriptor = activityProgressRegistry.progressAdd(inputDataUpload); + + GenericGuide guide = GenericGuide.create(activityProgressDescriptor, inputDataUpload, + SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found"))); + + // Validate the state + if (!inputDataUpload.getState().equals(UploadStatus.UPLOADED) + && !inputDataUpload.getState().equals(UploadStatus.ERROR)) { + // Error. Only files in UPLOADED state can process the LOAD operation + throw new RuntimeException("Only files in UPLOADED state can process the LOAD operation"); + } + + // Validate the Period (it's OPEN ?) + Period period = this.periodRepository.getReferenceById(inputDataUpload.getPeriod().getId()); + if (!period.getState().equals(PeriodStatus.OPENED)) { + throw new ImporterPeriodNotOpenedToReceiveDataRuntimeException(period.getName(), period.getState()); + } + + // Update state + inputDataUpload = self().updateStateWithNewTransaction(inputDataUpload, UploadStatus.PROCESSING); + guide.setActivityDomain(inputDataUpload); + + // Fire async activity + self().doInternalActivityAsync(guide); + + // Return without waiting + return new GenericResult(WorkResultEnum.COMPLETED); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public InputDataUpload updateStateWithNewTransaction(InputDataUpload inputDataUpload, UploadStatus newUploadStatus) { + inputDataUpload = inputDataUploadRepository.getReferenceById(inputDataUpload.getId()); + inputDataUpload.setState(newUploadStatus); + inputDataUpload = inputDataUploadRepository.save(inputDataUpload); + return inputDataUploadRepository.save(inputDataUpload); + } + + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + public CompletableFuture doInternalActivityAsync(GenericGuide guide) { + log.debug("Request to doInternalActivityAsync : {}", guide); + + GenericResult genericResult = null; + try { + genericResult = this.doInternalActivity(guide); + } catch (Throwable t) { + log.error("Error in doInternalActivityAsync : {}", t); + throw t; + } finally { + InputDataUpload inputDataUpload = guide.getActivityDomain(); + + //Always complete the activity and progress + if (genericResult == null || !genericResult.getWorkResult().equals(WorkResultEnum.COMPLETED)) { + inputDataUpload.setState(UploadStatus.ERROR); + guide.getActivityProgressDescriptor().updateProgress(100, "Error"); + } else { + inputDataUpload.setState(UploadStatus.PROCESSED); + guide.getActivityProgressDescriptor().updateProgress(100, "Completed"); + } + + this.inputDataUploadRepository.updateInNewTransaction(inputDataUpload); + } + + return CompletableFuture.completedFuture(genericResult); + } + + private GenericResult doInternalActivity(GenericGuide guide) { + log.debug("Request to doInternalActivity : {}", guide); + + InputDataUpload inputDataUpload = guide.getActivityDomain(); + + // Re-attach inputDataUpload + guide.setActivityDomain(entityManager.merge(inputDataUpload)); + log.debug("Request to doInternalActivity - merged inputDataUpload : {}", guide); + + TaskResult tr; + + // Task 1 - Clear logs and old data + guide.getActivityProgressDescriptor().updateProgress(15, "Cleanning logs"); + tr = this.clearInputDataUploadLogTask.doTask(guide); + if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) { + // STOP activity execution + return new GenericResult(WorkResultEnum.FAILED); + } + + // Task 2 - Clear old InputData, previously imported + guide.getActivityProgressDescriptor().updateProgress(30, "Cleanning previous imported data"); + tr = this.clearInputDataTask.doTask(guide); + if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) { + // STOP activity execution + return new GenericResult(WorkResultEnum.FAILED); + } + + // Task 3 - Import data from Excel file + guide.getActivityProgressDescriptor().updateProgress(45, "Importing data."); + InputDataImporterTask task = this.findImporterTask(inputDataUpload.getType()); + tr = task.doTask(guide); + if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) { + // STOP activity execution + return new GenericResult(WorkResultEnum.FAILED); + } + + // Count erros in InputDataUploadLog's + guide.getActivityProgressDescriptor().updateProgress(90, "Evaluating errors"); + GenericResult result = null; + Integer errorCount = self().countErrors(inputDataUpload); + if (errorCount > 0) { + inputDataUpload.setState(UploadStatus.ERROR); + result = new GenericResult(WorkResultEnum.FAILED); + } else { + inputDataUpload.setState(UploadStatus.PROCESSED); + result = new GenericResult(WorkResultEnum.COMPLETED); + } + + guide.getActivityProgressDescriptor().updateProgress(95, "Update state"); + inputDataUploadRepository.updateInNewTransaction(inputDataUpload); + guide.getActivityProgressDescriptor().updateProgress(100, "Finished"); + return result; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Integer countErrors(InputDataUpload inputDataUpload) { + Integer errorCount = inputDataUploadLogRepository.countByInputDataUpload(inputDataUpload); + return errorCount; + } + + private InputDataImporterTask findImporterTask(UploadType uploadType) { + for (InputDataImporterTask inputDataImporterTask : inputDataImporterTasks) { + if (inputDataImporterTask.getUploadType().equals(uploadType)) { + return inputDataImporterTask; + } + } + + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterThreadSafeContext.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterThreadSafeContext.java new file mode 100644 index 0000000..b632673 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/POIFileImporterThreadSafeContext.java @@ -0,0 +1,5 @@ +package com.oguerreiro.resilient.activities.inputDataImporter; + +public class POIFileImporterThreadSafeContext { + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/AbstractGlobalPOIFileImporterTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/AbstractGlobalPOIFileImporterTaskImpl.java new file mode 100644 index 0000000..2a91c96 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/AbstractGlobalPOIFileImporterTaskImpl.java @@ -0,0 +1,329 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.springframework.context.ApplicationContext; + +import com.oguerreiro.resilient.activity.AbstractTask; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.InputDataUploadLog; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.VariableClass; +import com.oguerreiro.resilient.domain.VariableUnits; +import com.oguerreiro.resilient.domain.enumeration.DataSourceType; +import com.oguerreiro.resilient.domain.enumeration.InputMode; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; +import com.oguerreiro.resilient.error.ExpressionEvaluationException; +import com.oguerreiro.resilient.error.ImporterOrganizationCodeNotFoundException; +import com.oguerreiro.resilient.error.ImporterOrganizationNotAccessibleException; +import com.oguerreiro.resilient.error.ImporterVariableValueException; +import com.oguerreiro.resilient.error.InventoryImportVariableClassNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportVariableInvalidValueForUnitTypeException; +import com.oguerreiro.resilient.error.InventoryImportVariableNotAllowedForDatafileException; +import com.oguerreiro.resilient.error.InventoryImportVariableNotAllowedForInputException; +import com.oguerreiro.resilient.error.InventoryImportVariableNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitConverterFormulaException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitConverterNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitNotAllowedException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportYearOfDataDontMatchRuntimeException; +import com.oguerreiro.resilient.error.ResilientException; +import com.oguerreiro.resilient.error.UnitConverterExpressionEvaluationRuntimeException; +import com.oguerreiro.resilient.expression.ExpressionUtils; +import com.oguerreiro.resilient.expression.UnitConverterExpressionContext; +import com.oguerreiro.resilient.expression.UnitConverterExpressionFunctions; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.repository.VariableRepository; + +public abstract class AbstractGlobalPOIFileImporterTaskImpl extends + AbstractTask, InputDataImporterTask> implements InputDataImporterTask { + private final VariableRepository variableRepository; + private final InputDataUploadLogRepository inputDataUploadLogRepository; + private final InputDataRepository inputDataRepository; + private final UnitRepository unitRepository; + private final EmissionFactorRepository emissionFactorRepository; + private final ApplicationContext applicationContext; + private final OrganizationRepository organizationRepository; + + public AbstractGlobalPOIFileImporterTaskImpl(VariableRepository variableRepository, + InputDataUploadLogRepository inputDataUploadLogRepository, InputDataRepository inputDataRepository, + UnitRepository unitRepository, EmissionFactorRepository emissionFactorRepository, + OrganizationRepository organizationRepository, ApplicationContext applicationContext) { + this.variableRepository = variableRepository; + this.inputDataUploadLogRepository = inputDataUploadLogRepository; + this.inputDataRepository = inputDataRepository; + this.unitRepository = unitRepository; + this.emissionFactorRepository = emissionFactorRepository; + this.applicationContext = applicationContext; + this.organizationRepository = organizationRepository; + } + + @Override + public GenericResult doTask(final GenericGuide guide) { + InputDataUpload inputDataUpload = guide.getActivityDomain(); + InputStream fileStream = new ByteArrayInputStream(inputDataUpload.getDataFile()); + + String currentProgressTask = guide.getActivityProgressDescriptor().getProgressTask(); + Integer currentProgressPercentage = guide.getActivityProgressDescriptor().getProgressPercentage(); + + try (GlobalPOIFileImporter importer = new GlobalPOIFileImporter(fileStream, null)) { + + importer.importPOI(new GlobalPOIFileImporterCallback() { + + @Override + public void callback(String sheetName, int rowNumber, String varCode, String varClassCode, + String varClassDescription, String varUnit, String varUnitCode, String varValue, + String varOrganizationCode) { + //Update progress description + guide.getActivityProgressDescriptor().updateProgress(currentProgressPercentage, + currentProgressTask + " [Sheet '" + sheetName + "']"); + + try { + AbstractGlobalPOIFileImporterTaskImpl.this.process(guide, sheetName, rowNumber, varCode, varClassCode, + varClassDescription, varUnit, varUnitCode, varValue, varOrganizationCode); + } catch (Throwable t) { + reportErrorInNewTransaction(inputDataUpload, guide.getUsername(), t); + } + } + + @Override + public void validateCallBack(Integer year) { + // Validate if year matches begin and end period date + int beginYear = inputDataUpload.getPeriod().getBeginDate().getYear(); + int endYear = inputDataUpload.getPeriod().getEndDate().getYear(); + if (!(beginYear == endYear && beginYear == year)) { + RuntimeException t = new InventoryImportYearOfDataDontMatchRuntimeException(endYear, year); + reportErrorInNewTransaction(inputDataUpload, guide.getUsername(), t); + throw t; + } + } + + }); + } catch (Throwable e) { + return new GenericResult(WorkResultEnum.FAILED); + } + + return new GenericResult(WorkResultEnum.COMPLETED); + } + + private void process(GenericGuide guide, String sheetName, int rowNumber, String varCode, + String varClassCode, String varClassDescription, String varUnit, String varUnitCode, String varValue, + String varOrganizationCode) throws ResilientException { + + // Validations ======================== + + // Variable exists ? + Variable variable = variableRepository.findOneByCodeWithToOneRelationships(varCode).orElse(null); + if (variable == null) { + throw new InventoryImportVariableNotFoundException(sheetName, rowNumber, varCode); + } + + // Variable is valid? (Input==TRUE && inputMode IN (ALL; DATAFILE)) + if (!variable.getInput()) { + throw new InventoryImportVariableNotAllowedForInputException(varCode); + } + + if (!List.of(InputMode.ANY, InputMode.DATAFILE).contains(variable.getInputMode())) { + throw new InventoryImportVariableNotAllowedForDatafileException(varCode); + } + + // Class exists? + String varClassName = null; + if (varClassCode != null && varClassCode.length() > 0) { + if (variable.getVariableClassType() == null) + throw new InventoryImportVariableClassNotFoundException(sheetName, rowNumber, varCode, varClassCode); + + List classes = variable.getVariableClassType().getVariableClasses().stream().filter( + c -> c.getCode().equals(varClassCode)).toList(); + if (classes.isEmpty()) + throw new InventoryImportVariableClassNotFoundException(sheetName, rowNumber, varCode, varClassCode); + + if (classes.size() > 1) + throw new RuntimeException("More than one class found for code [" + varClassCode + "]"); + + VariableClass varClass = classes.get(0); + varClassName = varClass.getName(); + } + + // Unit exists? + Unit sourceUnit = unitRepository.findOneByCodeOrSymbol(varUnitCode.toUpperCase(), varUnitCode).orElse(null); + if (sourceUnit == null) { + throw new InventoryImportVariableUnitNotFoundException(sheetName, rowNumber, varCode, varUnitCode); + } + + // And its allowed for this variable? + if (!variable.isUnitAllowed(sourceUnit)) { + throw new InventoryImportVariableUnitNotAllowedException(sheetName, rowNumber, varCode, varUnitCode); + } + + // Value must be of the type of the InputMode + if (!variable.getBaseUnit().getUnitType().getValueType().isValueValid(varValue)) { + throw new InventoryImportVariableInvalidValueForUnitTypeException(sheetName, rowNumber, varCode, varValue, + variable.getBaseUnit().getUnitType().getValueType().toString()); + } + + // Find Organization & validate if the current selected organization has access to it + Organization owner = this.organizationRepository.findOneByCode(varOrganizationCode).orElse(null); + if (owner == null) { + //Organization code not found + throw new ImporterOrganizationCodeNotFoundException(sheetName, rowNumber, varOrganizationCode); + } + + //Validate if the owner is accessible by the InputDataUpload. + if (!owner.equals(guide.getActivityDomain().getOwner()) && !owner.isChildOf(guide.getActivityDomain().getOwner())) { + //Can't import this value, because the owner it's not accesible by the declared InputDataUpload.owner. Only hierarchical organizations can import data from others + throw new ImporterOrganizationNotAccessibleException(sheetName, rowNumber, varCode, varOrganizationCode, + guide.getActivityDomain().getOwner().getCode()); + } + + // Insert InputData value + createAndInsertInputData(guide, sheetName, rowNumber, variable, varClassCode, varClassName, varClassDescription, + sourceUnit, varValue, owner); + } + + private void createAndInsertInputData(GenericGuide guide, String sheetName, int rowNumber, + Variable variable, String varClassCode, String varClassName, String varClassDescription, Unit sourceUnit, + String varValue, Organization owner) throws InventoryImportVariableUnitConverterNotFoundException, + InventoryImportVariableUnitConverterFormulaException, ImporterVariableValueException { + InputDataUpload inputDataUpload = guide.getActivityDomain(); + + // Convert value from a String to target value type + BigDecimal sourceValue; + BigDecimal variableValue = null; + String stringValue = null; + try { + if (sourceUnit.getUnitType().getValueType().equals(UnitValueType.STRING)) { + sourceValue = BigDecimal.ZERO; + variableValue = BigDecimal.ZERO; + stringValue = varValue; + } else if (sourceUnit.getUnitType().getValueType().equals(UnitValueType.DECIMAL)) { + sourceValue = this.convertToBigDecimal(varValue); + } else if (sourceUnit.getUnitType().getValueType().equals(UnitValueType.BOOLEAN)) { + // By convention. 0=FALSE; 1=TRUE + sourceValue = this.convertToBooleanAsBigDecimal(varValue); + } else { + throw new RuntimeException("Unexpected UnitValueType : " + sourceUnit.getUnitType().getValueType().toString()); + } + } catch (Throwable t) { + throw new ImporterVariableValueException(sheetName, rowNumber, variable.getCode(), + sourceUnit.getUnitType().getValueType().toString(), varValue); + } + + // The value needs conversion ? (only if valurType is NOT = STRING) + if (!sourceUnit.getUnitType().getValueType().equals(UnitValueType.STRING)) { + if (sourceUnit.equals(variable.getBaseUnit())) { + // Its the same. No need to convert value. + variableValue = sourceValue; + } else { + // Conversion is needed. The units are both of the same UniType ? + if (sourceUnit.getUnitType().equals(variable.getBaseUnit().getUnitType())) { + // Same UnitType, the conversion is just a scale conversion + // (sourceValue*CR(target unit)/CR(source unit)) + variableValue = sourceValue.multiply( + variable.getBaseUnit().getConvertionRate().divide(sourceUnit.getConvertionRate())); + } else { + // This is a more complex conversion. Needs a UnitConverter + Optional result = variable.getVariableUnits().stream().filter( + varUnit -> varUnit.getUnit().equals(sourceUnit)).findFirst(); + + if (!result.isPresent()) { + throw new InventoryImportVariableUnitConverterNotFoundException(sheetName, rowNumber, variable.getCode(), + sourceUnit.getCode(), variable.getBaseUnit().getCode()); + } + + // Evaluate formula to convert the value + UnitConverterExpressionFunctions expressionFunctions = new UnitConverterExpressionFunctions( + inputDataUpload.getPeriod()); + applicationContext.getAutowireCapableBeanFactory().autowireBean(expressionFunctions); //This is CRUCIAL, Without this there will be no dependency injection + + UnitConverterExpressionContext context = UnitConverterExpressionContext.create(emissionFactorRepository, + inputDataUpload.getPeriod().getBeginDate().getYear(), sourceValue, sourceUnit, variable.getBaseUnit(), + variable, varClassCode); + context.setExpressionFunctions(expressionFunctions); + + VariableUnits variableUnit = result.orElseThrow(() -> new RuntimeException("Value not found")); + UnitConverter unitConverter = variableUnit.getUnitConverter(); + String convertionFormula = unitConverter.getConvertionFormula(); + + try { + Object convertedValue = ExpressionUtils.evalExpression(convertionFormula, context); + variableValue = this.toBigDecimal(convertedValue); + } catch (ExpressionEvaluationException e) { + throw new UnitConverterExpressionEvaluationRuntimeException(variable.getCode(), sourceUnit.getCode(), + variable.getBaseUnit().getCode(), sourceValue.toString(), e); + } + } + } + } + + InputData inputData = new InputData(); + inputData.owner(owner).period(inputDataUpload.getPeriod()).variable(variable).sourceType( + DataSourceType.FILE).sourceInputDataUpload(inputDataUpload).sourceUnit(sourceUnit).sourceValue( + sourceValue).variableValue(variableValue).imputedValue(variableValue).stringValue(stringValue).creationDate( + Instant.now()).creationUsername(guide.getUsername()).changeDate(Instant.now()).changeUsername( + guide.getUsername()).unit(variable.getBaseUnit()).variableClassCode(varClassCode).variableClassName( + varClassName); + + inputDataRepository.saveInNewTransaction(inputData); + } + + private BigDecimal convertToBigDecimal(String value) { + try { + return new BigDecimal(value); + } catch (Throwable t) { + //Unable to convert value to number + throw t; + } + } + + private BigDecimal convertToBooleanAsBigDecimal(String value) { + List validValuesTrue = Arrays.asList("1", "TRUE", "YES", "SIM", "VERDADEIRO"); + List validValuesFalse = Arrays.asList("0", "FALSE", "NO", "NAO", "NÃO", "FALSO"); + String auxValue = value.trim().toUpperCase(); + + if (validValuesTrue.contains(value)) { + return BigDecimal.ONE; + } + if (validValuesFalse.contains(value)) { + return BigDecimal.ZERO; + } + + throw new RuntimeException("Invalid boolean value."); + } + + private void reportErrorInNewTransaction(InputDataUpload inputDataUpload, String username, Throwable error) { + InputDataUploadLog log = new InputDataUploadLog(); + + String localizedMessage = getLocalizedMessage(error); + + log.inputDataUpload(inputDataUpload).creationDate(Instant.now()).creationUsername(username).logMessage( + localizedMessage); + + this.inputDataUploadLogRepository.saveInNewTransaction(log); + } + + private BigDecimal toBigDecimal(Object value) { + if (Integer.class.isInstance(value)) { + return new BigDecimal((Integer) value); + } + + return (BigDecimal) value; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/EnergyPOIFileImporterTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/EnergyPOIFileImporterTaskImpl.java new file mode 100644 index 0000000..8409a8a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/EnergyPOIFileImporterTaskImpl.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.repository.VariableRepository; + +@Component +public class EnergyPOIFileImporterTaskImpl extends AbstractGlobalPOIFileImporterTaskImpl { + + public EnergyPOIFileImporterTaskImpl(VariableRepository variableRepository, + InputDataUploadLogRepository inputDataUploadLogRepository, InputDataRepository inputDataRepository, + UnitRepository unitRepository, EmissionFactorRepository emissionFactorRepository, + OrganizationRepository organizationRepository, ApplicationContext applicationContext) { + super(variableRepository, inputDataUploadLogRepository, inputDataRepository, unitRepository, + emissionFactorRepository, organizationRepository, applicationContext); + } + + @Override + public UploadType getUploadType() { + return UploadType.ENERGY; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GasPOIFileImporterTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GasPOIFileImporterTaskImpl.java new file mode 100644 index 0000000..6ce001e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GasPOIFileImporterTaskImpl.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.repository.VariableRepository; + +@Component +public class GasPOIFileImporterTaskImpl extends AbstractGlobalPOIFileImporterTaskImpl { + + public GasPOIFileImporterTaskImpl(VariableRepository variableRepository, + InputDataUploadLogRepository inputDataUploadLogRepository, InputDataRepository inputDataRepository, + UnitRepository unitRepository, EmissionFactorRepository emissionFactorRepository, + OrganizationRepository organizationRepository, ApplicationContext applicationContext) { + super(variableRepository, inputDataUploadLogRepository, inputDataRepository, unitRepository, + emissionFactorRepository, organizationRepository, applicationContext); + } + + @Override + public UploadType getUploadType() { + return UploadType.GAS; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporter.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporter.java new file mode 100644 index 0000000..ba5ecf4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporter.java @@ -0,0 +1,286 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; + +public class GlobalPOIFileImporter implements Closeable { + private static final int MAX_TRY_ROWS = 30; + + private GlobalPOIFileImporterCallback callback; + private InputStream poiInputStream; + private String password; + + public GlobalPOIFileImporter(InputStream poiInputStream, String password) { + this.poiInputStream = poiInputStream; + this.password = password; + } + + public GlobalPOIFileImporter(File poiFile, String password) throws FileNotFoundException { + this(new FileInputStream(poiFile), password); + } + + public void importPOI(File poiFile, GlobalPOIFileImporterCallback callback) + throws EncryptedDocumentException, IOException { + this.callback = callback; + this.loadPOIFile(); + } + + public void importPOI(GlobalPOIFileImporterCallback callback) throws EncryptedDocumentException, IOException { + this.callback = callback; + this.loadPOIFile(); + } + + private void loadPOIFile() throws EncryptedDocumentException, IOException { + this.loadWorkbook(); + } + + private void loadWorkbook() throws EncryptedDocumentException, IOException { + + try (Workbook workbook = WorkbookFactory.create(this.poiInputStream, this.password)) { + //First sheet have the year value + Cell yearCell = workbook.getSheetAt(0).getRow(5).getCell(7); + Integer year = this.getCellValueDouble(yearCell).intValue(); + this.callback.validateCallBack(year); + + //Loop all sheets + for (Sheet sheet : workbook) { + //Ignore the intro sheet. No data there for me + if (sheet.getSheetName().toUpperCase().equals("INTRO")) + continue; + + this.loadSheet(sheet); + } + } + } + + private void loadSheet(Sheet sheet) { + try { + this.printDataHeader(sheet.getSheetName()); + } catch (Throwable t) { + //Ignore. Don't stop the import, because of logging errors + t.printStackTrace(); + } + + //Loop all rows + int maxTry = 0; + int lastRowNumber = 0; + for (Row row : sheet) { + if ((row.getRowNum() - lastRowNumber) > MAX_TRY_ROWS || maxTry == MAX_TRY_ROWS) { + //Stop reading. No more data rows + break; + } + lastRowNumber = row.getRowNum(); + + if (this.isRowData(row)) { + maxTry = 0; + if (this.hasData(row)) { + this.loadRow(row); + } + } else { + maxTry++; + } + } + } + + private void loadRow(Row row) { + String varCode = this.getCellValueString(row.getCell(1)); // B + String varClass = this.removeLineBreaks(this.getCellValueString(row.getCell(3))); // D + + if (varClass == null || varClass.length() == 0) { + //Check if this is a merged CELL. Get the value of the merged range + varClass = this.getMergedCellValue(row.getCell(3)); + } + String varClassCode = this.processClassCode(varClass); + String varClassDescription = this.processClassDescription(varClass); + + String varUnit = this.getCellValueString(row.getCell(6)); // G + String varUnitCode = this.processUnit(varUnit); + + String varOrganizationCode = this.getCellValueString(row.getCell(2)); // C + + String varValue = this.getCellValueString(row.getCell(7)); // H + + this.callback.callback(row.getSheet().getSheetName(), row.getRowNum(), varCode, varClassCode, varClassDescription, + varUnit, varUnitCode, varValue, varOrganizationCode); + this.printData(varCode, varClassCode, varUnit, varUnitCode, varValue, varOrganizationCode); + } + + private String processClassCode(String varClass) { + if (varClass == null) { + return varClass; + } + + String code = null; + if (varClass.contains("[") && varClass.contains("]")) { + //Extract code between [..] + varClass = varClass.trim(); + String lastChar = varClass.substring(varClass.length() - 1, varClass.length()); + if (!lastChar.equals("]")) { + //ERROR + } else { + int startCodeIndex = varClass.lastIndexOf("["); + code = varClass.substring(startCodeIndex + 1, varClass.length() - 1); + } + } + + return code; + } + + private String processClassDescription(String varClass) { + if (varClass == null) { + return varClass; + } + + String description = varClass; + if (varClass.contains("[")) { + //Extract code between [..] + int startCodeIndex = varClass.lastIndexOf("["); + description = varClass.substring(0, startCodeIndex + 1); + } + + return description; + } + + private String processUnit(String varUnit) { + if (varUnit == null) { + return varUnit; + } + + String code = null; + code = StringUtils.stripAccents(varUnit.trim()); //Uppercase && Trim && Strip accents + + String finalCode = ""; + for (char ch : code.toCharArray()) { + if ((ch > 'a' && ch < 'z') || (ch > 'A' && ch < 'Z') || (ch > '0' && ch < '9') || ch == '%' || ch == '€') { + finalCode += ch; + } else { + finalCode += '_'; + } + } + + return finalCode; + } + + private void printDataHeader(String sheetName) { + System.out.printf( + "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------%n"); + System.out.printf("-- %s --%n", sheetName); + System.out.printf( + "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------%n"); + System.out.printf("| %-10s | %-20s | %-25s | %-25s | %10s | %-35s |", "CODE", "CLASS CODE", "UNIT", "UNIT CODE", + "VALUE", "OWNER"); + System.out.printf( + "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------%n"); + } + + private void printData(String varCode, String varClass, String varUnit, String varUnitCode, String varValue, + String varOrganizationCode) { + if (varClass == null) + varClass = ""; + System.out.printf("| %-10s | %-20s | %-25s | %-25s | %10s | %-35s |", varCode, varClass, varUnit, varUnitCode, + varValue, varOrganizationCode); + } + + private boolean isRowData(Row row) { + String varCode = this.getCellValueString(row.getCell(1)); //B + + if (varCode != null && varCode.length() > 0) { + return true; + } + + return false; + } + + private boolean hasData(Row row) { + String varCode = this.getCellValueString(row.getCell(1)); // B + String varOrganizationCode = this.getCellValueString(row.getCell(2)); // C + String varClass = this.getCellValueString(row.getCell(3)); // D + String varUnit = this.getCellValueString(row.getCell(6)); // G + String varValue = this.getCellValueString(row.getCell(7)); // H + + //Check if it has data to load + if (hasValue(varCode) && hasValue(varOrganizationCode) && hasValue(varValue) && hasValue(varUnit)) { + return true; + } + + return false; + } + + private boolean hasValue(String value) { + return (value != null) && (value.length() > 0); + } + + private String removeLineBreaks(String value) { + if (value == null) + return null; + + return value.replace("\n", "").replace("\r", ""); + } + + private String getCellValueString(Cell cell) { + String value; + if (cell == null) + return null; + + try { + value = cell.getStringCellValue().trim(); + } catch (Exception e) { + //Try numeric + double valueDouble = cell.getNumericCellValue(); + value = String.valueOf(valueDouble); + } + + return value; + } + + private Double getCellValueDouble(Cell cell) { + try { + return cell.getNumericCellValue(); + } catch (Throwable t) { + return null; + } + } + + public String getMergedCellValue(Cell cell) { + CellRangeAddress range = getMergedRegionForCell(cell); + if (range != null) { + Sheet s = cell.getRow().getSheet(); + Cell valueCell = s.getRow(range.getFirstRow()).getCell(range.getFirstColumn()); + return this.getCellValueString(valueCell); + } + + return null; + } + + public CellRangeAddress getMergedRegionForCell(Cell c) { + Sheet s = c.getRow().getSheet(); + for (CellRangeAddress mergedRegion : s.getMergedRegions()) { + if (mergedRegion.isInRange(c.getRowIndex(), c.getColumnIndex())) { + // This region contains the cell in question + return mergedRegion; + } + } + // Not in any + return null; + } + + @Override + public void close() throws IOException { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporterCallback.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporterCallback.java new file mode 100644 index 0000000..13712e0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporterCallback.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +public interface GlobalPOIFileImporterCallback { + void callback(String sheetName, int row, String varCode, String varClassCode, String varClassDescription, + String varUnit, String varUnitCode, String varValue, String varOrganizationCode); + + void validateCallBack(Integer year); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporterTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporterTaskImpl.java new file mode 100644 index 0000000..7c11a15 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/GlobalPOIFileImporterTaskImpl.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.repository.VariableRepository; + +@Component +public class GlobalPOIFileImporterTaskImpl extends AbstractGlobalPOIFileImporterTaskImpl { + + public GlobalPOIFileImporterTaskImpl(VariableRepository variableRepository, + InputDataUploadLogRepository inputDataUploadLogRepository, InputDataRepository inputDataRepository, + UnitRepository unitRepository, EmissionFactorRepository emissionFactorRepository, + OrganizationRepository organizationRepository, ApplicationContext applicationContext) { + super(variableRepository, inputDataUploadLogRepository, inputDataRepository, unitRepository, + emissionFactorRepository, organizationRepository, applicationContext); + } + + @Override + public UploadType getUploadType() { + return UploadType.GLOBAL; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InputDataImporterTask.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InputDataImporterTask.java new file mode 100644 index 0000000..89342b2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InputDataImporterTask.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.Task; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.enumeration.UploadType; + +public interface InputDataImporterTask extends Task> { + UploadType getUploadType(); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporter.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporter.java new file mode 100644 index 0000000..bf17ad4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporter.java @@ -0,0 +1,307 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; + +public class InventoryPOIFileImporter implements Closeable { + private static final int MAX_TRY_ROWS = 30; + + private InventoryPOIFileImporterCallback callback; + private InputStream poiInputStream; + private String password; + + public InventoryPOIFileImporter(InputStream poiInputStream, String password) { + this.poiInputStream = poiInputStream; + this.password = password; + } + + public InventoryPOIFileImporter(File poiFile, String password) throws FileNotFoundException { + this(new FileInputStream(poiFile), password); + } + + public void importPOI(File poiFile, InventoryPOIFileImporterCallback callback) + throws EncryptedDocumentException, IOException { + this.callback = callback; + this.loadPOIFile(); + } + + public void importPOI(InventoryPOIFileImporterCallback callback) throws EncryptedDocumentException, IOException { + this.callback = callback; + this.loadPOIFile(); + } + + private void loadPOIFile() throws EncryptedDocumentException, IOException { + this.loadWorkbook(); + } + + private void loadWorkbook() throws EncryptedDocumentException, IOException { + + try (Workbook workbook = WorkbookFactory.create(this.poiInputStream, this.password)) { + //Loop all sheets + for (Sheet sheet : workbook) { + //Ignore the intro sheet. No data there for me + if (sheet.getSheetName().toUpperCase().equals("INTRO")) { + //Read the UO code, and call the ownerCallBack() for validation + Cell uoCell = sheet.getRow(3).getCell(18); + Cell yearCell = sheet.getRow(5).getCell(18); + String uoCode = this.getCellValueString(uoCell); + Double year = this.getCellValueDouble(yearCell); + this.callback.validateCallBack(uoCode, year.intValue()); + continue; + } + + this.loadSheet(sheet); + } + } + } + + private void loadSheet(Sheet sheet) { + this.printDataHeader(sheet.getSheetName()); + + //Loop all rows + int maxTry = 0; + boolean isFirst = true; + boolean ignoreNext = false; + int lastRowNumber = 0; + for (Row row : sheet) { + if ((row.getRowNum() - lastRowNumber) > MAX_TRY_ROWS || maxTry == MAX_TRY_ROWS) { + //Stop reading. No more data rows + break; + } + lastRowNumber = row.getRowNum(); + + if (ignoreNext) { + ignoreNext = false; + continue; + } + if (this.isRowData(row)) { + if (isFirst) { + //The first row with value in CELL("B" | 1), its a HEADER. IGNORE IT. And IGNORE the next row also (merged cell) + isFirst = false; + ignoreNext = true; + } else { + maxTry = 0; + if (this.hasData(row)) { + this.loadRow(row); + } + } + } else { + isFirst = true; //Reset. Because the next value will be a HEADER again + maxTry++; + } + } + } + + private void loadRow(Row row) { + String varCode = this.getCellValueString(row.getCell(1)); + String varClass = this.removeLineBreaks(this.getCellValueString(row.getCell(3))); + if (varClass == null || varClass.length() == 0) { + //Check if this is a merged CELL. Get the value of the merged range + varClass = this.getMergedCellValue(row.getCell(3)); + } + + String varClassCode = this.processClassCode(varClass); + String varClassDescription = this.processClassDescription(varClass); + + String varUnit = this.removeLineBreaks(this.getCellValueString(row.getCell(6))); + String varUnitCode = this.processUnit(varUnit); + String varValue = this.getCellValueString(row.getCell(7)); + String varSource = this.getCellValueString(row.getCell(8)); + if (varSource == null || varSource.length() == 0) { + //Check if this is a merged CELL. Get the value of the merged range + varSource = this.getMergedCellValue(row.getCell(8)); + } + String varPerson = this.getCellValueString(row.getCell(9)); + if (varPerson == null || varPerson.length() == 0) { + //Check if this is a merged CELL. Get the value of the merged range + varPerson = this.getMergedCellValue(row.getCell(9)); + } + String varNotes = this.getCellValueString(row.getCell(10)); + if (varNotes == null || varNotes.length() == 0) { + //Check if this is a merged CELL. Get the value of the merged range + varNotes = this.getMergedCellValue(row.getCell(10)); + } + + this.callback.callback(row.getSheet().getSheetName(), row.getRowNum(), varCode, varClassCode, varClassDescription, + varUnit, varUnitCode, varValue, varSource, varPerson, varNotes); + this.printData(varCode, varClassCode, varUnit, varUnitCode, varValue, varSource, varPerson, varNotes); + } + + private String processClassCode(String varClass) { + if (varClass == null) { + return varClass; + } + + String code = null; + if (varClass.contains("[") && varClass.contains("]")) { + //Extract code between [..] + varClass = varClass.trim(); + String lastChar = varClass.substring(varClass.length() - 1, varClass.length()); + if (!lastChar.equals("]")) { + //ERROR + } else { + int startCodeIndex = varClass.lastIndexOf("["); + code = varClass.substring(startCodeIndex + 1, varClass.length() - 1); + } + } + + return code; + } + + private String processClassDescription(String varClass) { + if (varClass == null) { + return varClass; + } + + String description = varClass; + if (varClass.contains("[")) { + //Extract code between [..] + int startCodeIndex = varClass.lastIndexOf("["); + description = varClass.substring(0, startCodeIndex + 1); + } + + return description; + } + + private String processUnit(String varUnit) { + if (varUnit == null) { + return varUnit; + } + + String code = null; + code = StringUtils.stripAccents(varUnit.toUpperCase().trim()); //Uppercase && Trim && Strip accents + + String finalCode = ""; + for (char ch : code.toCharArray()) { + if (ch < 'A' || ch > 'Z') { + ch = '_'; + } + + finalCode += ch; + } + + return finalCode; + } + + private void printDataHeader(String sheetName) { + System.out.printf( + "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------%n"); + System.out.printf("-- %s --%n", sheetName); + System.out.printf( + "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------%n"); + System.out.printf("| %-10s | %-20s | %-25s | %-25s | %10s | %-35s | %-15s | %-30s |%n", "CODE", "CLASS CODE", + "UNIT", "UNIT CODE", "VALUE", "SOURCE", "PERSON", "NOTES"); + System.out.printf( + "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------%n"); + } + + private void printData(String varCode, String varClass, String varUnit, String varUnitCode, String varValue, + String varSource, String varPerson, String varnotes) { + if (varClass == null) + varClass = ""; + if (varPerson == null) + varPerson = ""; + if (varSource == null) + varSource = ""; + if (varnotes == null) + varnotes = ""; + System.out.printf("| %-10s | %-20s | %-25s | %-25s | %10s | %-35s | %-15s | %-30s |%n", varCode, varClass, varUnit, + varUnitCode, varValue, varSource, varPerson, varnotes); + } + + private boolean isRowData(Row row) { + String varCode = this.getCellValueString(row.getCell(1)); + + if (varCode != null && varCode.length() > 0) { + return true; + } + + return false; + } + + private boolean hasData(Row row) { + String varCode = this.getCellValueString(row.getCell(1)); + String varValue = this.getCellValueString(row.getCell(7)); + + //Check if it has data to load + if (varCode != null && varValue != null && varCode.length() > 0 && varValue.length() > 0) { + return true; + } + + return false; + } + + private String removeLineBreaks(String value) { + if (value == null) + return null; + + return value.replace("\n", "").replace("\r", ""); + } + + private String getCellValueString(Cell cell) { + String value; + if (cell == null) + return null; + + try { + value = cell.getStringCellValue().trim(); + } catch (Exception e) { + //Try numeric + double valueDouble = cell.getNumericCellValue(); + value = String.valueOf(valueDouble); + } + + return value; + } + + private Double getCellValueDouble(Cell cell) { + try { + return cell.getNumericCellValue(); + } catch (Throwable t) { + return null; + } + } + + public String getMergedCellValue(Cell cell) { + CellRangeAddress range = getMergedRegionForCell(cell); + if (range != null) { + Sheet s = cell.getRow().getSheet(); + Cell valueCell = s.getRow(range.getFirstRow()).getCell(range.getFirstColumn()); + return this.getCellValueString(valueCell); + } + + return null; + } + + public CellRangeAddress getMergedRegionForCell(Cell c) { + Sheet s = c.getRow().getSheet(); + for (CellRangeAddress mergedRegion : s.getMergedRegions()) { + if (mergedRegion.isInRange(c.getRowIndex(), c.getColumnIndex())) { + // This region contains the cell in question + return mergedRegion; + } + } + // Not in any + return null; + } + + @Override + public void close() throws IOException { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporterCallback.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporterCallback.java new file mode 100644 index 0000000..45cd1dc --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporterCallback.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +public interface InventoryPOIFileImporterCallback { + void callback(String sheetName, int row, String varCode, String varClassCode, String varClassDescription, + String varUnit, String varUnitCode, String varValue, String varSource, String varPerson, String varnotes); + + void validateCallBack(String uoCode, Integer year); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporterTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporterTaskImpl.java new file mode 100644 index 0000000..f2ca4ad --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/inputDataImporter/importers/InventoryPOIFileImporterTaskImpl.java @@ -0,0 +1,313 @@ +package com.oguerreiro.resilient.activities.inputDataImporter.importers; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.activity.AbstractTask; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.InputDataUploadLog; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.VariableClass; +import com.oguerreiro.resilient.domain.VariableUnits; +import com.oguerreiro.resilient.domain.enumeration.DataSourceType; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.error.ExpressionEvaluationException; +import com.oguerreiro.resilient.error.ImporterVariableValueException; +import com.oguerreiro.resilient.error.InventoryImportOrganizationDontMatchRuntimeException; +import com.oguerreiro.resilient.error.InventoryImportVariableClassNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportVariableInvalidValueForUnitTypeException; +import com.oguerreiro.resilient.error.InventoryImportVariableNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitConverterFormulaException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitConverterNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitNotAllowedException; +import com.oguerreiro.resilient.error.InventoryImportVariableUnitNotFoundException; +import com.oguerreiro.resilient.error.InventoryImportYearOfDataDontMatchRuntimeException; +import com.oguerreiro.resilient.error.ResilientException; +import com.oguerreiro.resilient.error.UnitConverterExpressionEvaluationRuntimeException; +import com.oguerreiro.resilient.expression.ExpressionUtils; +import com.oguerreiro.resilient.expression.UnitConverterExpressionContext; +import com.oguerreiro.resilient.expression.UnitConverterExpressionFunctions; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.repository.VariableRepository; + +@Component +public class InventoryPOIFileImporterTaskImpl extends + AbstractTask, InputDataImporterTask> implements InputDataImporterTask { + private final VariableRepository variableRepository; + private final InputDataUploadLogRepository inputDataUploadLogRepository; + private final InputDataRepository inputDataRepository; + private final UnitRepository unitRepository; + private final EmissionFactorRepository emissionFactorRepository; + private final ApplicationContext applicationContext; + + public InventoryPOIFileImporterTaskImpl(VariableRepository variableRepository, + InputDataUploadLogRepository inputDataUploadLogRepository, InputDataRepository inputDataRepository, + UnitRepository unitRepository, EmissionFactorRepository emissionFactorRepository, + ApplicationContext applicationContext) { + this.variableRepository = variableRepository; + this.inputDataUploadLogRepository = inputDataUploadLogRepository; + this.inputDataRepository = inputDataRepository; + this.unitRepository = unitRepository; + this.emissionFactorRepository = emissionFactorRepository; + this.applicationContext = applicationContext; + } + + @Override + public GenericResult doTask(final GenericGuide guide) { + InputDataUpload inputDataUpload = guide.getActivityDomain(); + InputStream fileStream = new ByteArrayInputStream(inputDataUpload.getDataFile()); + + String currentProgressTask = guide.getActivityProgressDescriptor().getProgressTask(); + Integer currentProgressPercentage = guide.getActivityProgressDescriptor().getProgressPercentage(); + + try (InventoryPOIFileImporter importer = new InventoryPOIFileImporter(fileStream, "InNOVA")) { + + importer.importPOI(new InventoryPOIFileImporterCallback() { + + @Override + public void callback(String sheetName, int rowNumber, String varCode, String varClassCode, + String varClassDescription, String varUnit, String varUnitCode, String varValue, String varSource, + String varPerson, String varnotes) { + //Update progress description + guide.getActivityProgressDescriptor().updateProgress(currentProgressPercentage, + currentProgressTask + " [Sheet '" + sheetName + "']"); + + try { + InventoryPOIFileImporterTaskImpl.this.process(guide, sheetName, rowNumber, varCode, varClassCode, + varClassDescription, varUnit, varUnitCode, varValue, varSource, varPerson, varnotes); + } catch (Throwable t) { + reportErrorInNewTransaction(inputDataUpload, guide.getUsername(), t); + } + } + + @Override + public void validateCallBack(String uoCode, Integer year) { + + // Validate is uoCode matches the InputDataUpload.Owner + if (!inputDataUpload.getOwner().getCode().equals(uoCode)) { + RuntimeException t = new InventoryImportOrganizationDontMatchRuntimeException( + inputDataUpload.getOwner().getCode(), uoCode); + reportErrorInNewTransaction(inputDataUpload, guide.getUsername(), t); + throw t; + } + + // Validate if year matches begin and end period date + int beginYear = inputDataUpload.getPeriod().getBeginDate().getYear(); + int endYear = inputDataUpload.getPeriod().getEndDate().getYear(); + if (!(beginYear == endYear && beginYear == year)) { + RuntimeException t = new InventoryImportYearOfDataDontMatchRuntimeException(endYear, year); + reportErrorInNewTransaction(inputDataUpload, guide.getUsername(), t); + throw t; + } + } + + }); + } catch (IOException e) { + return new GenericResult(WorkResultEnum.FAILED); + } + + return new GenericResult(WorkResultEnum.COMPLETED); + } + + private void process(GenericGuide guide, String sheetName, int rowNumber, String varCode, + String varClassCode, String varClassDescription, String varUnit, String varUnitCode, String varValue, + String varSource, String varPerson, String varnotes) throws ResilientException { + + // Validations ======================== + + // Variable exists ? + Variable variable = variableRepository.findOneByCodeWithToOneRelationships(varCode).orElse(null); + if (variable == null) { + throw new InventoryImportVariableNotFoundException(sheetName, rowNumber, varCode); + } + + // Class exists? + String varClassName = null; + if (varClassCode != null && varClassCode.length() > 0) { + if (variable.getVariableClassType() == null) + throw new InventoryImportVariableClassNotFoundException(sheetName, rowNumber, varCode, varClassCode); + + List classes = variable.getVariableClassType().getVariableClasses().stream().filter( + c -> c.getCode().equals(varClassCode)).toList(); + if (classes.isEmpty()) + throw new InventoryImportVariableClassNotFoundException(sheetName, rowNumber, varCode, varClassCode); + + if (classes.size() > 1) + throw new RuntimeException("More than one class found for code [" + varClassCode + "]"); + + VariableClass varClass = classes.get(0); + varClassName = varClass.getName(); + } + + // Unit exists? + Unit sourceUnit = unitRepository.findOneByCodeOrSymbol(varUnitCode.toUpperCase(), varUnitCode).orElse(null); + if (sourceUnit == null) { + throw new InventoryImportVariableUnitNotFoundException(sheetName, rowNumber, varCode, varUnitCode); + } + + // And its allowed for this variable? + if (!variable.isUnitAllowed(sourceUnit)) { + throw new InventoryImportVariableUnitNotAllowedException(sheetName, rowNumber, varCode, varUnitCode); + } + + // Value must be of the type of the InputMode + if (!variable.getBaseUnit().getUnitType().getValueType().isValueValid(varValue)) { + throw new InventoryImportVariableInvalidValueForUnitTypeException(sheetName, rowNumber, varCode, varValue, + variable.getBaseUnit().getUnitType().getValueType().toString()); + } + + // Insert InputData value + createAndInsertInputData(guide, sheetName, rowNumber, variable, varClassCode, varClassName, varClassDescription, + sourceUnit, varValue, varSource, varPerson, varnotes); + } + + private void createAndInsertInputData(GenericGuide guide, String sheetName, int rowNumber, + Variable variable, String varClassCode, String varClassName, String varClassDescription, Unit sourceUnit, + String varValue, String varSource, String varPerson, String varnotes) + throws InventoryImportVariableUnitConverterNotFoundException, + InventoryImportVariableUnitConverterFormulaException, ImporterVariableValueException { + InputDataUpload inputDataUpload = guide.getActivityDomain(); + + // Convert value from a String to target value type + BigDecimal sourceValue; + try { + if (sourceUnit.getUnitType().getValueType().equals(UnitValueType.DECIMAL)) { + sourceValue = this.convertToBigDecimal(varValue); + } else if (sourceUnit.getUnitType().getValueType().equals(UnitValueType.BOOLEAN)) { + // By convention. 0=FALSE; 1=TRUE + sourceValue = this.convertToBooleanAsBigDecimal(varValue); + } else { + throw new RuntimeException("Unexpected UnitValueType : " + sourceUnit.getUnitType().getValueType().toString()); + } + } catch (Throwable t) { + throw new ImporterVariableValueException(sheetName, rowNumber, variable.getCode(), + sourceUnit.getUnitType().getValueType().toString(), varValue); + } + + // The value needs conversion ? + BigDecimal variableValue = null; + if (sourceUnit.equals(variable.getBaseUnit())) { + // Its the same. No need to convert value. + variableValue = sourceValue; + } else { + // Conversion is needed. The units are both of the same UniType ? + if (sourceUnit.getUnitType().equals(variable.getBaseUnit().getUnitType())) { + // Same UnitType, the conversion is just a scale conversion + // (sourceValue*CR(target unit)/CR(source unit)) + variableValue = sourceValue.multiply( + variable.getBaseUnit().getConvertionRate().divide(sourceUnit.getConvertionRate())); + } else { + // This is a more complex conversion. Needs a UnitConverter + Optional result = variable.getVariableUnits().stream().filter( + varUnit -> varUnit.getUnit().equals(sourceUnit)).findFirst(); + + if (!result.isPresent()) { + throw new InventoryImportVariableUnitConverterNotFoundException(sheetName, rowNumber, variable.getCode(), + sourceUnit.getCode(), variable.getBaseUnit().getCode()); + } + + // Evaluate formula to convert the value + UnitConverterExpressionFunctions expressionFunctions = new UnitConverterExpressionFunctions( + inputDataUpload.getPeriod()); + applicationContext.getAutowireCapableBeanFactory().autowireBean(expressionFunctions); //This is CRUCIAL, Without this there will be no dependency injection + + UnitConverterExpressionContext context = UnitConverterExpressionContext.create(emissionFactorRepository, + inputDataUpload.getPeriod().getBeginDate().getYear(), sourceValue, sourceUnit, variable.getBaseUnit(), + variable, varClassCode); + context.setExpressionFunctions(expressionFunctions); + + VariableUnits variableUnit = result.orElseThrow(() -> new RuntimeException("Value not found")); + UnitConverter unitConverter = variableUnit.getUnitConverter(); + String convertionFormula = unitConverter.getConvertionFormula(); + + try { + Object convertedValue = ExpressionUtils.evalExpression(convertionFormula, context); + variableValue = this.toBigDecimal(convertedValue); + } catch (ExpressionEvaluationException e) { + throw new UnitConverterExpressionEvaluationRuntimeException(variable.getCode(), sourceUnit.getCode(), + variable.getBaseUnit().getCode(), sourceValue.toString(), e); + } + } + } + + InputData inputData = new InputData(); + inputData.owner(inputDataUpload.getOwner()).period(inputDataUpload.getPeriod()).variable(variable).sourceType( + DataSourceType.FILE).sourceInputDataUpload(inputDataUpload).sourceUnit(sourceUnit).sourceValue( + sourceValue).variableValue(variableValue).imputedValue(variableValue) // The same has variabeValue. + // For now, there's no way of + // variableValue distribution + // to other Organizations. + .dataSource(varSource).dataUser(varPerson).dataComments(varnotes).creationDate(Instant.now()).creationUsername( + guide.getUsername()).changeDate(Instant.now()).changeUsername(guide.getUsername()).unit( + variable.getBaseUnit()).variableClassCode(varClassCode).variableClassName(varClassName); + + inputDataRepository.saveInNewTransaction(inputData); + } + + private BigDecimal convertToBigDecimal(String value) { + try { + return new BigDecimal(value); + } catch (Throwable t) { + //Unable to convert value to number + throw t; + } + } + + private BigDecimal convertToBooleanAsBigDecimal(String value) { + List validValuesTrue = Arrays.asList("1", "TRUE", "YES", "SIM", "VERDADEIRO"); + List validValuesFalse = Arrays.asList("0", "FALSE", "NO", "NAO", "NÃO", "FALSO"); + String auxValue = value.trim().toUpperCase(); + + if (validValuesTrue.contains(value)) { + return BigDecimal.ONE; + } + if (validValuesFalse.contains(value)) { + return BigDecimal.ZERO; + } + + throw new RuntimeException("Invalid boolean value."); + } + + private void reportErrorInNewTransaction(InputDataUpload inputDataUpload, String username, Throwable error) { + InputDataUploadLog log = new InputDataUploadLog(); + + String localizedMessage = getLocalizedMessage(error); + + log.inputDataUpload(inputDataUpload).creationDate(Instant.now()).creationUsername(username).logMessage( + localizedMessage); + + this.inputDataUploadLogRepository.saveInNewTransaction(log); + } + + private BigDecimal toBigDecimal(Object value) { + if (Integer.class.isInstance(value)) { + return new BigDecimal((Integer) value); + } + + return (BigDecimal) value; + } + + @Override + public UploadType getUploadType() { + return UploadType.INVENTORY; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/ClearResilientLogTask.java b/src/main/java/com/oguerreiro/resilient/activities/period/ClearResilientLogTask.java new file mode 100644 index 0000000..6d85a94 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/ClearResilientLogTask.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.activities.period; + +import com.oguerreiro.resilient.activity.ActivityDomain; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.Task; + +public interface ClearResilientLogTask extends Task> { + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/ClearResilientLogTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/period/ClearResilientLogTaskImpl.java new file mode 100644 index 0000000..af324f9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/ClearResilientLogTaskImpl.java @@ -0,0 +1,34 @@ +package com.oguerreiro.resilient.activities.period; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.activity.AbstractTask; +import com.oguerreiro.resilient.activity.ActivityDomain; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.service.ResilientLogService; + +@Qualifier("myClearResilientLogTask") +@Component +public class ClearResilientLogTaskImpl extends + AbstractTask, ClearResilientLogTask> implements ClearResilientLogTask { + + private final ResilientLogService resilientLogService; + + public ClearResilientLogTaskImpl(ResilientLogService resilientLogService) { + this.resilientLogService = resilientLogService; + } + + @Override + public GenericResult doTask(GenericGuide guide) { + ActivityDomain activitydomain = guide.getActivityDomain(); + + //Clear logs for the activity domain entity + resilientLogService.deleteAllByOwnerIdWithNewTransaction(activitydomain.getId()); + + return new GenericResult(WorkResultEnum.COMPLETED); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/close/PeriodCloseActivity.java b/src/main/java/com/oguerreiro/resilient/activities/period/close/PeriodCloseActivity.java new file mode 100644 index 0000000..5579413 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/close/PeriodCloseActivity.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.activities.period.close; + +import java.util.concurrent.CompletableFuture; + +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.domain.Period; + +public interface PeriodCloseActivity extends Activity> { + CompletableFuture doInternalActivityAsync(GenericGuide guide); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/close/PeriodCloseActivityImpl.java b/src/main/java/com/oguerreiro/resilient/activities/period/close/PeriodCloseActivityImpl.java new file mode 100644 index 0000000..055436d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/close/PeriodCloseActivityImpl.java @@ -0,0 +1,73 @@ +package com.oguerreiro.resilient.activities.period.close; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.activity.AbstractActivity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.repository.PeriodRepository; + +@Component +public class PeriodCloseActivityImpl extends AbstractActivity, PeriodCloseActivity> implements PeriodCloseActivity { + + private static final String ACTIVITY_KEY = "period-close"; + + private final PeriodRepository periodRepository; + private final ActivityProgressRegistry activityProgressRegistry; + + /* ----- Constructor ----- */ + public PeriodCloseActivityImpl(PeriodRepository periodRepository, ActivityProgressRegistry activityProgressRegistry) { + this.periodRepository = periodRepository; + this.activityProgressRegistry = activityProgressRegistry; + } + + /* ----- SelectionStrategy properties ----- */ + @Override + public Class appliesToActivityDomainClass() { + return Period.class; + } + + @Override + public List appliesToStates() { + return Arrays.asList(PeriodStatus.OPENED.toString()); + } + + /* ----- Activity implementation ----- */ + @Override + public String getActivityKey() { + return ACTIVITY_KEY; + } + + @Transactional + @Override + public GenericResult doActivity(String activityDomainClass, Long activityDomainId) { + // Load Period + Period period = periodRepository.getReferenceById(activityDomainId); + + // Validations + if (!period.getState().equals(PeriodStatus.OPENED)) { + throw new RuntimeException("Activity Close, applies only to Opened period state."); + } + + // Change status + period.setState(PeriodStatus.CLOSED); + + // Return without waiting + return new GenericResult(WorkResultEnum.COMPLETED); + } + + @Override + public CompletableFuture doInternalActivityAsync(GenericGuide guide) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/open/PeriodOpenActivity.java b/src/main/java/com/oguerreiro/resilient/activities/period/open/PeriodOpenActivity.java new file mode 100644 index 0000000..dd046c9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/open/PeriodOpenActivity.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.activities.period.open; + +import java.util.concurrent.CompletableFuture; + +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.domain.Period; + +public interface PeriodOpenActivity extends Activity> { + CompletableFuture doInternalActivityAsync(GenericGuide guide); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/open/PeriodOpenActivityImpl.java b/src/main/java/com/oguerreiro/resilient/activities/period/open/PeriodOpenActivityImpl.java new file mode 100644 index 0000000..1e19f3a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/open/PeriodOpenActivityImpl.java @@ -0,0 +1,73 @@ +package com.oguerreiro.resilient.activities.period.open; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.activity.AbstractActivity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.repository.PeriodRepository; + +@Component +public class PeriodOpenActivityImpl extends AbstractActivity, PeriodOpenActivity> implements PeriodOpenActivity { + + private static final String ACTIVITY_KEY = "period-open"; + + private final PeriodRepository periodRepository; + private final ActivityProgressRegistry activityProgressRegistry; + + /* ----- Constructor ----- */ + public PeriodOpenActivityImpl(PeriodRepository periodRepository, ActivityProgressRegistry activityProgressRegistry) { + this.periodRepository = periodRepository; + this.activityProgressRegistry = activityProgressRegistry; + } + + /* ----- SelectionStrategy properties ----- */ + @Override + public Class appliesToActivityDomainClass() { + return Period.class; + } + + @Override + public List appliesToStates() { + return Arrays.asList(PeriodStatus.CREATED.toString()); + } + + /* ----- Activity implementation ----- */ + @Override + public String getActivityKey() { + return ACTIVITY_KEY; + } + + @Transactional + @Override + public GenericResult doActivity(String activityDomainClass, Long activityDomainId) { + // Load Period + Period period = periodRepository.getReferenceById(activityDomainId); + + // Validations + if (!period.getState().equals(PeriodStatus.CREATED)) { + throw new RuntimeException("Activity Open, applies only to CREATED period state."); + } + + // Change status + period.setState(PeriodStatus.OPENED); + + // Return without waiting + return new GenericResult(WorkResultEnum.COMPLETED); + } + + @Override + public CompletableFuture doInternalActivityAsync(GenericGuide guide) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/process/ClearOutputDataTask.java b/src/main/java/com/oguerreiro/resilient/activities/period/process/ClearOutputDataTask.java new file mode 100644 index 0000000..2601fb0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/process/ClearOutputDataTask.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.activities.period.process; + +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.Task; +import com.oguerreiro.resilient.domain.Period; + +public interface ClearOutputDataTask extends Task> { + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/process/ClearOutputDataTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/period/process/ClearOutputDataTaskImpl.java new file mode 100644 index 0000000..c731542 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/process/ClearOutputDataTaskImpl.java @@ -0,0 +1,36 @@ +package com.oguerreiro.resilient.activities.period.process; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.activity.AbstractTask; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.service.OutputDataService; + +@Qualifier("myClearOutputDataTask") +@Component +public class ClearOutputDataTaskImpl extends AbstractTask, ClearOutputDataTask> + implements ClearOutputDataTask { + + private final OutputDataService outputDataService; + + public ClearOutputDataTaskImpl(OutputDataService outputDataService) { + this.outputDataService = outputDataService; + } + + @Override + public GenericResult doTask(GenericGuide guide) { + PeriodVersion periodVersion = (PeriodVersion) guide.otherValues.get( + PeriodProcessOutputsActivity.GUIDE_OTHERVALUE_PERIODVERSION); + + // Clear logs for InputdataUpload entity. With new transaction + outputDataService.deleteAllByPeriodVersionInNewTransaction(periodVersion); + + return new GenericResult(WorkResultEnum.COMPLETED); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/process/GenerateOutputDataTask.java b/src/main/java/com/oguerreiro/resilient/activities/period/process/GenerateOutputDataTask.java new file mode 100644 index 0000000..0cb1af2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/process/GenerateOutputDataTask.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.activities.period.process; + +import java.util.List; + +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.Task; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.expression.OutputVariableExpressionFunctions; + +public interface GenerateOutputDataTask extends Task> { + void calculateOrganization(GenericGuide guide, Organization organization, List outputVariables); + + void calculateOutput(Variable outputVariable, Period period, PeriodVersion periodVersion, Organization organization, + OutputVariableExpressionFunctions expressionFunctions); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/process/GenerateOutputDataTaskImpl.java b/src/main/java/com/oguerreiro/resilient/activities/period/process/GenerateOutputDataTaskImpl.java new file mode 100644 index 0000000..48e3a98 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/process/GenerateOutputDataTaskImpl.java @@ -0,0 +1,385 @@ +package com.oguerreiro.resilient.activities.period.process; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.activity.AbstractTask; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.error.ExpressionEvaluationException; +import com.oguerreiro.resilient.error.OutputExpressionEvaluationRuntimeException; +import com.oguerreiro.resilient.error.OutputExpressionNullResultRuntimeException; +import com.oguerreiro.resilient.error.PeriodProcessDependenciesNotSatisfyedException; +import com.oguerreiro.resilient.expression.ExpressionUtils; +import com.oguerreiro.resilient.expression.FunctionCall; +import com.oguerreiro.resilient.expression.OutputVariableExpressionContext; +import com.oguerreiro.resilient.expression.OutputVariableExpressionFunctions; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.OutputDataRepository; +import com.oguerreiro.resilient.repository.VariableRepository; + +@Qualifier("myGenerateOutputDataTask") +@Component +public class GenerateOutputDataTaskImpl extends + AbstractTask, GenerateOutputDataTask> implements GenerateOutputDataTask { + + private final OutputDataRepository outputDataRepository; + private final VariableRepository variableRepository; + private final OrganizationRepository organizationRepository; + private final EmissionFactorRepository emissionFactorRepository; + private final InputDataRepository inputDataRepository; + private final ApplicationContext applicationContext; + + public GenerateOutputDataTaskImpl(VariableRepository variableRepository, + OrganizationRepository organizationRepository, EmissionFactorRepository emissionFactorRepository, + InputDataRepository inputDataRepository, ApplicationContext applicationContext, + OutputDataRepository outputDataRepository) { + this.variableRepository = variableRepository; + this.organizationRepository = organizationRepository; + this.emissionFactorRepository = emissionFactorRepository; + this.inputDataRepository = inputDataRepository; + this.applicationContext = applicationContext; + this.outputDataRepository = outputDataRepository; + } + + @Override + public GenericResult doTask(GenericGuide guide) { + this.calculateGlobalOutputs(guide); + this.calculateSingleOutputs(guide); + + return new GenericResult(WorkResultEnum.COMPLETED); + } + + private void calculateGlobalOutputs(GenericGuide guide) { + Integer progressTotalTaskPercentage = 70; + + // Get all output NOT single variables + List outputVariables = variableRepository.findAllOutputNotSingle(); + + String progressTaskText = guide.getActivityProgressDescriptor().getProgressTask(); + Integer progressPercentage = guide.getActivityProgressDescriptor().getProgressPercentage(); + + // Create an calculation plan. This ensures the correct order of calculation by dependencies + guide.getActivityProgressDescriptor().updateProgress(progressPercentage, + progressTaskText + " - Create calculation plan"); + outputVariables = calculatePlan(outputVariables); + + // Get a list of all Organizations with output generation + List outputOrgs = organizationRepository.findAllOrganizationForOutput(); + Integer countOrgs = outputOrgs.size(); + Integer progressOrgPercentage = progressTotalTaskPercentage / countOrgs; + guide.otherValues.put("PROGRESS_ORG", progressOrgPercentage); + Integer processedOrgs = 0; + + for (Organization organization : outputOrgs) { + // Refresh progress & update task text + progressOrgPercentage = processedOrgs * progressTotalTaskPercentage / countOrgs; + guide.getActivityProgressDescriptor().updateProgress(progressPercentage + progressOrgPercentage, + progressTaskText); + + // Calculate Org + this.logInfo(guide.getActivityDomain(), guide.getUsername(), + "Started Output Calculation for " + organization.getCode()); + self().calculateOrganization(guide, organization, outputVariables); + this.logInfo(guide.getActivityDomain(), guide.getUsername(), + "Ended Output Calculation for " + organization.getCode()); + + processedOrgs++; + } + } + + private void calculateSingleOutputs(GenericGuide guide) { + // Get all output single variables + List outputVariables = variableRepository.findAllOutputSingle(); + + for (Variable variable : outputVariables) { + self().calculateOrganization(guide, variable.getOutputOwner(), List.of(variable)); + } + } + + private List calculatePlan(List outputVariables) + throws PeriodProcessDependenciesNotSatisfyedException { + List sortedOutputVariables = new ArrayList(); + List sortedOutputVariableCodes = new ArrayList(); //For debug purpose only + Map> variableDependencies = new HashMap>(); + Map> variableWildcardDependencies = new HashMap>(); + Set dependencies; + boolean hasWildcard = false; + + // Create a list of variables and its dependencies + for (Variable variable : outputVariables) { + //Initialize + dependencies = new HashSet(); + hasWildcard = false; + + //Calculate dependencies list + List functionCalls = extractFunctionCalls(variable.getOutputFormula(), "out"); + + for (FunctionCall fCall : functionCalls) { + String code = extractVariableCodeFromOutFunction(fCall.getArguments()); + if (code != null) { + dependencies.add(code); + if (code.contains("*")) + hasWildcard = true; + } + } + + variableDependencies.put(variable, dependencies); + if (hasWildcard) + variableWildcardDependencies.put(variable, dependencies); + } + + // Remove wildcard dependencies, by replacing with match variable codes + for (Entry> entryWild : variableWildcardDependencies.entrySet()) { + dependencies = new HashSet(); + + for (String dependency : entryWild.getValue()) { + if (!dependency.contains("*")) { + dependencies.add(dependency); + } else { + for (Entry> entryVar : variableDependencies.entrySet()) { + if (entryVar.getKey().equals(entryWild.getKey())) + continue; //Ignore it self + if (wildcardMatch(entryVar.getKey().getCode(), dependency)) + dependencies.add(entryVar.getKey().getCode()); + } + } + } + + variableDependencies.put(entryWild.getKey(), dependencies); + } + + List selectedOutputVariables = new ArrayList(); + List selectedOutputVariableCodes = new ArrayList(); + List previousOutputVariableCodes = new ArrayList(); + + // Loop until variableDependencies collection have zero elements + do { + selectedOutputVariables.clear(); + selectedOutputVariableCodes.clear(); + + // Filter Variables that have no dependencies. They can be added to the stack + for (Entry> entry : variableDependencies.entrySet()) { + //Remove previous sorted dependency codes + dependencies = entry.getValue(); + dependencies.removeAll(previousOutputVariableCodes); + + //Update the dependencies list of the variable + variableDependencies.put(entry.getKey(), dependencies); + + //Check if all dependencies are already sorted + if (dependencies.size() == 0) { + selectedOutputVariables.add(entry.getKey()); + selectedOutputVariableCodes.add(entry.getKey().getCode()); + } + } + + //Sort by code, just to always have the same sequence (its easier when debugging) + selectedOutputVariables.sort((s1, s2) -> s1.getCode().compareTo(s2.getCode())); + + //Add to the sorted list + sortedOutputVariables.addAll(selectedOutputVariables); + sortedOutputVariableCodes.addAll(selectedOutputVariableCodes); + + //Remove from the unsorted MAP + variableDependencies.keySet().removeAll(selectedOutputVariables); + + //Move the selectedOutputVariables to the previousOutputVariableCodes + previousOutputVariableCodes.clear(); + previousOutputVariableCodes.addAll(selectedOutputVariableCodes); + + if (selectedOutputVariables.size() == 0 && variableDependencies.size() > 0) { + //Something is wrong. There's still variables with dependencies, but no variables were selected in the last loop. + //OR we have circular references. OR there are references, that are not present in the list. + PeriodProcessDependenciesNotSatisfyedException ex = new PeriodProcessDependenciesNotSatisfyedException( + variableDependencies); + ex.printDependencies(); + throw ex; + } + } while (variableDependencies.size() > 0); + + return sortedOutputVariables; + } + + /** + * Search a string {@code text} to see if it matches the given {@code pattern}.
+ * Example: {@code text = "010203"} , {@code pattern = "0102*"} , will return {@code true} + * + * @param text + * @param pattern + * @return + */ + public static boolean wildcardMatch(String text, String pattern) { + // Replace '*' with '.*' (matches zero or more characters) + // Replace '?' with '.' (matches exactly one character) + String regex = pattern.replace("?", ".").replace("*", ".*"); + + // Match the text against the regular expression + return Pattern.matches(regex, text); + } + + private List extractFunctionCalls(String mvelExpression, String targetFunction) { + List functionCalls = new ArrayList(); + + if (mvelExpression == null || mvelExpression.trim().length() == 0) { + return functionCalls; + } + + // Regular expression to match function calls + String regex = "(\\w+)\\(([^)]*)\\)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(mvelExpression); + + while (matcher.find()) { + String functionName = matcher.group(1); // Function name + String arguments = matcher.group(2); // Argument list as a single string + + if (targetFunction != null && !functionName.equals(targetFunction)) { + // Ignore this functions + continue; + } + + // Split arguments, considering commas and trim spaces + List args = splitArguments(arguments); + + functionCalls.add(new FunctionCall(functionName, args)); + } + + return functionCalls; + } + + // Helper method to split arguments safely + private static List splitArguments(String arguments) { + List result = new ArrayList<>(); + + int depth = 0; + StringBuilder currentArg = new StringBuilder(); + + for (char c : arguments.toCharArray()) { + if (c == ',' && depth == 0) { + // Argument separator at the top level + result.add(currentArg.toString().trim()); + currentArg.setLength(0); + } else { + // Adjust depth for nested parentheses + if (c == '(') + depth++; + if (c == ')') + depth--; + + currentArg.append(c); + } + } + + // Add the last argument + if (currentArg.length() > 0) { + result.add(currentArg.toString().trim()); + } + + return result; + } + + private String extractVariableCodeFromOutFunction(List arguments) { + int size = arguments.size(); + + if (size >= 1) { + //The code is always the first argument + String code = arguments.get(0); + + //Remove the commas + code = code.replace("\"", ""); + + return code; + } + + return null; + } + + public void calculateOrganization(GenericGuide guide, Organization organization, + List outputVariables) { + Period period = guide.getActivityDomain(); + PeriodVersion periodVersion = (PeriodVersion) guide.otherValues.get( + PeriodProcessOutputsActivity.GUIDE_OTHERVALUE_PERIODVERSION); + + Integer countOutputVariables = outputVariables.size(); + Integer calculatedOutputVariables = 0; + String progressTaskText = guide.getActivityProgressDescriptor().getProgressTask(); + Integer progressPercentage = guide.getActivityProgressDescriptor().getProgressPercentage(); + Integer progressOrgPercentage = (Integer) guide.otherValues.get("PROGRESS_ORG"); + Integer progressOutputPercentage; + + String orgProgressTaskText = progressTaskText + " - Calculating " + organization.getCode(); + guide.getActivityProgressDescriptor().updateProgress(progressPercentage, orgProgressTaskText); + + OutputVariableExpressionFunctions expressionFunctions = new OutputVariableExpressionFunctions(period, periodVersion, + organization); + applicationContext.getAutowireCapableBeanFactory().autowireBean(expressionFunctions); //This is CRUCIAL, Without this there will be no dependency injection + + for (Variable variable : outputVariables) { + self().calculateOutput(variable, period, periodVersion, organization, expressionFunctions); + + // Progress + calculatedOutputVariables++; + progressOutputPercentage = calculatedOutputVariables * progressOrgPercentage / outputVariables.size(); + guide.getActivityProgressDescriptor().updateProgress(progressPercentage + progressOutputPercentage, + orgProgressTaskText + " [" + calculatedOutputVariables + " / " + countOutputVariables + "]"); + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void calculateOutput(Variable outputVariable, Period period, PeriodVersion periodVersion, + Organization organization, OutputVariableExpressionFunctions expressionFunctions) { + // Evaluate outputFormula + OutputVariableExpressionContext context = OutputVariableExpressionContext.create(emissionFactorRepository, + inputDataRepository, outputVariable, period, periodVersion, organization); + context.setExpressionFunctions(expressionFunctions); + + Object result; + String expression = outputVariable.getOutputFormula(); + try { + result = ExpressionUtils.evalExpression(expression, context); + } catch (ExpressionEvaluationException e) { + throw new OutputExpressionEvaluationRuntimeException(outputVariable.getCode(), organization.getCode(), + period.getName(), periodVersion.getName(), expression, e); + } + + if (result == null) { + throw new OutputExpressionNullResultRuntimeException(outputVariable.getCode(), organization.getCode(), + period.getName(), periodVersion.getName(), expression); + } + + // Insert output, in new transation + OutputData outputData = new OutputData().baseUnit(outputVariable.getBaseUnit()).owner(organization).period( + period).periodVersion(periodVersion).value( + ExpressionUtils.toBigDecimal(result, outputVariable.getValueScale())).variable(outputVariable); + try { + outputDataRepository.save(outputData); + } catch (Throwable t) { + // Something happened when saving the outputData. Log the record for debugging + log.error("Error saving OutputData : " + outputData.toString(), t); + throw t; + } + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/process/PeriodProcessOutputsActivity.java b/src/main/java/com/oguerreiro/resilient/activities/period/process/PeriodProcessOutputsActivity.java new file mode 100644 index 0000000..f3e96ea --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/process/PeriodProcessOutputsActivity.java @@ -0,0 +1,18 @@ +package com.oguerreiro.resilient.activities.period.process; + +import java.util.concurrent.CompletableFuture; + +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; + +public interface PeriodProcessOutputsActivity extends Activity> { + public static final String GUIDE_OTHERVALUE_PERIODVERSION = "PERIODVERSION"; + + CompletableFuture doInternalActivityAsync(GenericGuide guide); + + Period updateStateWithNewTransaction(Period period, PeriodStatus periodStatus, PeriodVersion newPeriodVersion); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/process/PeriodProcessOutputsActivityImpl.java b/src/main/java/com/oguerreiro/resilient/activities/period/process/PeriodProcessOutputsActivityImpl.java new file mode 100644 index 0000000..ea15bcf --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/process/PeriodProcessOutputsActivityImpl.java @@ -0,0 +1,266 @@ +package com.oguerreiro.resilient.activities.period.process; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.activities.period.ClearResilientLogTask; +import com.oguerreiro.resilient.activity.AbstractActivity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.TaskResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor; +import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.domain.enumeration.PeriodVersionStatus; +import com.oguerreiro.resilient.repository.PeriodRepository; +import com.oguerreiro.resilient.repository.PeriodVersionRepository; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.service.PeriodService; + +import jakarta.persistence.EntityManager; + +@Component +public class PeriodProcessOutputsActivityImpl + extends AbstractActivity, PeriodProcessOutputsActivity> + implements PeriodProcessOutputsActivity { + + private final Logger log = LoggerFactory.getLogger(PeriodProcessOutputsActivityImpl.class); + + private static final String ACTIVITY_KEY = "period-process-outputs"; + + /* ----- TASKS ----- */ + private final ClearResilientLogTask clearResilientLogTask; + private final ClearOutputDataTask clearOutputDataTask; + private final GenerateOutputDataTask generateOutputDataTask; + + /* ----- BEANS ----- */ + private final PeriodRepository periodRepository; + private final PeriodService periodService; + private final ActivityProgressRegistry activityProgressRegistry; + private final EntityManager entityManager; + + /* ----- Constructor ----- */ + public PeriodProcessOutputsActivityImpl(PeriodRepository periodRepository, + ActivityProgressRegistry activityProgressRegistry, PeriodVersionRepository periodVersionRepository, + EntityManager entityManager, ClearOutputDataTask clearOutputDataTask, + GenerateOutputDataTask generateOutputDataTask, ClearResilientLogTask clearResilientLogTask, + PeriodService periodService) { + this.periodRepository = periodRepository; + this.activityProgressRegistry = activityProgressRegistry; + this.periodService = periodService; + this.entityManager = entityManager; + this.clearOutputDataTask = clearOutputDataTask; + this.generateOutputDataTask = generateOutputDataTask; + this.clearResilientLogTask = clearResilientLogTask; + } + + /* ----- SelectionStrategy properties ----- */ + @Override + public Class appliesToActivityDomainClass() { + return Period.class; + } + + @Override + public List appliesToStates() { + return Arrays.asList(PeriodStatus.CLOSED.toString(), PeriodStatus.PROCESSING.toString()); + } + + /* ----- Activity implementation ----- */ + @Override + public String getActivityKey() { + return ACTIVITY_KEY; + } + + @Transactional + @Override + public GenericResult doActivity(String activityDomainClass, Long activityDomainId) { + // Load Period + Period period = periodRepository.getReferenceById(activityDomainId); + + // Validations + if (!period.getState().equals(PeriodStatus.CLOSED) && !period.getState().equals(PeriodStatus.PROCESSING)) { + throw new RuntimeException("Activity Process Outputs, applies only to CLOSED period state."); + } + + if (period.getState().equals(PeriodStatus.PROCESSING)) { + // Must have a PeriodVersion in state ERROR or PROCESSING (if something critical + // happened to the server before) + // TODO + } else { + // In this case, can't have pending PeriodVersions. Search for any 'non-ended' + // PeriodVersion + boolean hasActivePeriodVersions = period.getPeriodVersions().stream().anyMatch( + periodVersion -> !periodVersion.getState().equals(PeriodVersionStatus.ENDED)); + if (hasActivePeriodVersions) { + throw new RuntimeException("The period have PeriodVersion's not ENDED. Process the PeriodVersion instead."); + } + } + + PeriodVersion periodVersion = null; + + if (period.getState().equals(PeriodStatus.CLOSED)) { + // Create a PeriodVersion, only when the Period is CLOSE (not reprocessing) + + // Get the max PeriodVersion + int periodVersionIndex = 1; + PeriodVersion maxPeriodVersion = period.getPeriodVersions().stream().max( + Comparator.comparingInt(PeriodVersion::getPeriodVersion)).orElse(null); + if (maxPeriodVersion != null) { + periodVersionIndex = maxPeriodVersion.getPeriodVersion() + 1; + } + + // Create PeriodVersion to (really) process the outputs + periodVersion = new PeriodVersion(); + periodVersion.name(period.getName() + " - " + periodVersionIndex).creationDate(Instant.now()).creationUsername( + SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found"))).description( + period.getName() + " - " + periodVersionIndex).period(period).periodVersion(periodVersionIndex).state( + PeriodVersionStatus.PROCESSING); + } + + // Save & Change status + period = self().updateStateWithNewTransaction(period, PeriodStatus.PROCESSING, periodVersion); + + // Fire PeriodVersion activity + ActivityProgressDescriptor activityProgressDescriptor = activityProgressRegistry.progressAdd(period); + GenericGuide guide = GenericGuide.create(activityProgressDescriptor, period, + SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found"))); + self().doInternalActivityAsync(guide); + + // Return without waiting + return new GenericResult(WorkResultEnum.COMPLETED); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Period updateStateWithNewTransaction(Period period, PeriodStatus periodStatus, + PeriodVersion newPeriodVersion) { + period = periodRepository.getReferenceById(period.getId()); + period.setState(periodStatus); + + if (newPeriodVersion != null) { + period.addPeriodVersion(newPeriodVersion); + } + + return periodRepository.save(period); + } + + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public CompletableFuture doInternalActivityAsync(GenericGuide guide) { + log.debug("Request to doInternalActivityAsync : {}", guide); + + GenericResult genericResult = null; + try { + genericResult = this.doInternalActivity(guide); + + guide.getActivityProgressDescriptor().updateProgress(100, "Finished"); + + //Update Period & PeriodVersion state + Period period = guide.getActivityDomain(); + PeriodVersion periodVersion = (PeriodVersion) guide.otherValues.get(GUIDE_OTHERVALUE_PERIODVERSION); + + period.setState(PeriodStatus.ENDED); + periodVersion.setState(PeriodVersionStatus.ENDED); + periodRepository.save(period); + + } catch (Throwable t) { + log.error("Error in doInternalActivityAsync : {}", t); + Period period = guide.getActivityDomain(); + + this.logError(period, guide.getUsername(), t); + + // Stop progress + guide.getActivityProgressDescriptor().updateProgress(100, "Error"); + + // Update PeriodVersion state to ERROR + PeriodVersion periodVersion = (PeriodVersion) guide.otherValues.get(GUIDE_OTHERVALUE_PERIODVERSION); + periodVersion.setState(PeriodVersionStatus.ERROR); + periodService.saveWithNewTransaction(period); // The current transaction will be rollbacked, thats why it has to be done in a new transaction + + throw t; + } + + return CompletableFuture.completedFuture(genericResult); + } + + private GenericResult doInternalActivity(GenericGuide guide) { + log.debug("Request to doInternalActivity : {}", guide); + + Period period = guide.getActivityDomain(); + + // Re-attach period + guide.setActivityDomain(entityManager.merge(period)); + period = guide.getActivityDomain(); + log.debug("Request to doInternalActivity - merged period : {}", guide); + + PeriodVersion periodVersion = null; + // Get the processing PeriodVersion. One of PROCESSING or ERROR. But only one + List periodVersions = period.getPeriodVersions().stream().filter( + pv -> !pv.getState().equals(PeriodVersionStatus.ENDED)).collect(Collectors.toList()); + + if (periodVersions.size() != 1) { + throw new RuntimeException( + "More than one PeriodVersion was found, ready for PROCESSING. [" + periodVersions.size() + "]"); + } + + periodVersion = periodVersions.get(0); + + guide.otherValues.put(PeriodProcessOutputsActivity.GUIDE_OTHERVALUE_PERIODVERSION, periodVersion); + + TaskResult tr; + + // Task 0 - Clear logs + guide.getActivityProgressDescriptor().updateProgress(5, "Clear logs"); + tr = this.clearResilientLogTask.doTask(guide); + if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) { + // STOP activity execution + return new GenericResult(WorkResultEnum.FAILED); + } + + // Task 1 - Clear any OutputData currently generated for the Period and + // PeriodVersion + // Probably a re-processing + guide.getActivityProgressDescriptor().updateProgress(15, "Clear previous outputs"); + tr = this.clearOutputDataTask.doTask(guide); + if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) { + // STOP activity execution + this.logError(periodVersion, guide.getUsername(), "Clear previous outputs - FAILED"); + return new GenericResult(WorkResultEnum.FAILED); + } + this.logInfo(periodVersion, guide.getUsername(), "Cleared previous outputs"); + + // Task 2 - Generate outputs + guide.getActivityProgressDescriptor().updateProgress(30, "Calculate Outputs"); + tr = this.generateOutputDataTask.doTask(guide); + if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) { + // STOP activity execution + this.logError(periodVersion, guide.getUsername(), "Calculate Outputs - FAILED"); + return new GenericResult(WorkResultEnum.FAILED); + } + this.logInfo(periodVersion, guide.getUsername(), "Calculated Outputs"); + + // If the progress got this far, no errors were throwned + // Update progress + guide.getActivityProgressDescriptor().updateProgress(90, "Update state"); + GenericResult result = null; + + //Create result response for activity + result = new GenericResult(WorkResultEnum.COMPLETED); + + return result; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/reopen/PeriodReopenActivity.java b/src/main/java/com/oguerreiro/resilient/activities/period/reopen/PeriodReopenActivity.java new file mode 100644 index 0000000..5bf7b54 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/reopen/PeriodReopenActivity.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.activities.period.reopen; + +import java.util.concurrent.CompletableFuture; + +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.domain.Period; + +public interface PeriodReopenActivity extends Activity> { + CompletableFuture doInternalActivityAsync(GenericGuide guide); +} diff --git a/src/main/java/com/oguerreiro/resilient/activities/period/reopen/PeriodReopenActivityImpl.java b/src/main/java/com/oguerreiro/resilient/activities/period/reopen/PeriodReopenActivityImpl.java new file mode 100644 index 0000000..a8b8403 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activities/period/reopen/PeriodReopenActivityImpl.java @@ -0,0 +1,76 @@ +package com.oguerreiro.resilient.activities.period.reopen; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.activity.AbstractActivity; +import com.oguerreiro.resilient.activity.GenericGuide; +import com.oguerreiro.resilient.activity.GenericResult; +import com.oguerreiro.resilient.activity.WorkResultEnum; +import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.repository.PeriodRepository; + +@Component +public class PeriodReopenActivityImpl extends + AbstractActivity, PeriodReopenActivity> implements PeriodReopenActivity { + + private static final String ACTIVITY_KEY = "period-reopen"; + + private final PeriodRepository periodRepository; + private final ActivityProgressRegistry activityProgressRegistry; + + /* ----- Constructor ----- */ + public PeriodReopenActivityImpl(PeriodRepository periodRepository, + ActivityProgressRegistry activityProgressRegistry) { + this.periodRepository = periodRepository; + this.activityProgressRegistry = activityProgressRegistry; + } + + /* ----- SelectionStrategy properties ----- */ + @Override + public Class appliesToActivityDomainClass() { + return Period.class; + } + + @Override + public List appliesToStates() { + return Arrays.asList(PeriodStatus.CLOSED.toString(), PeriodStatus.ENDED.toString()); + } + + /* ----- Activity implementation ----- */ + @Override + public String getActivityKey() { + return ACTIVITY_KEY; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public GenericResult doActivity(String activityDomainClass, Long activityDomainId) { + // Load Period + Period period = periodRepository.getReferenceById(activityDomainId); + + // Validations + if (!period.getState().equals(PeriodStatus.CLOSED) && !period.getState().equals(PeriodStatus.ENDED)) { + throw new RuntimeException("Activity Close, applies only to Opened period state."); + } + + // Change status + period.setState(PeriodStatus.OPENED); + + // Return without waiting + return new GenericResult(WorkResultEnum.COMPLETED); + } + + @Override + public CompletableFuture doInternalActivityAsync(GenericGuide guide) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/AbstractActivity.java b/src/main/java/com/oguerreiro/resilient/activity/AbstractActivity.java new file mode 100644 index 0000000..a5f3168 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/AbstractActivity.java @@ -0,0 +1,100 @@ +package com.oguerreiro.resilient.activity; + +import java.time.Instant; +import java.util.Locale; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.i18n.LocaleContextHolder; + +import com.oguerreiro.resilient.domain.AbstractResilientLongIdEntity; +import com.oguerreiro.resilient.domain.ResilientLog; +import com.oguerreiro.resilient.domain.enumeration.ResilientLogLevel; +import com.oguerreiro.resilient.error.ResilientException; +import com.oguerreiro.resilient.error.ResilientRuntimeException; +import com.oguerreiro.resilient.service.ResilientLogService; + +/** + * @param the {@link ActivityResult} to use as response for this {@link Activity} + * @param the {@link ActivityGuide} to use as configuration for this {@link Activity} + * @param the interface that defines the Activity bean. Will be used to automatically infer the bean and supply the + * self() bean reference + */ +public abstract class AbstractActivity> + extends AbstractSelfEnabler implements Activity { + + @Autowired + private ResilientLogService resilientLogService; + + @Autowired + private MessageSource messageSource; + + public final String getLocalizedMessage(Throwable exception) { + String localizedMessage = ""; + Locale locale = LocaleContextHolder.getLocale(); + + if (exception instanceof ResilientException) { + //This is a translatable Resilient message + ResilientException resilientException = (ResilientException) exception; + try { + localizedMessage = messageSource.getMessage(resilientException.getMessageCode(), + resilientException.getMessageArgs(), locale); + } catch (NoSuchMessageException ex) { + // Fall back to exception LocalizedMessage. + localizedMessage = exception.getLocalizedMessage(); + } + + if (localizedMessage == null) { + localizedMessage = resilientException.getMessageCode(); + } + } else if (exception instanceof ResilientRuntimeException) { + //This is a translatable Resilient message + ResilientRuntimeException resilientException = (ResilientRuntimeException) exception; + try { + localizedMessage = messageSource.getMessage(resilientException.getMessageCode(), + resilientException.getMessageArgs(), locale); + } catch (NoSuchMessageException ex) { + // Fall back to exception LocalizedMessage. + localizedMessage = exception.getLocalizedMessage(); + } + + if (localizedMessage == null) { + localizedMessage = resilientException.getMessageCode(); + } + } else { + localizedMessage = exception.getLocalizedMessage(); + } + + return localizedMessage; + } + + protected void logError(AbstractResilientLongIdEntity id, String username, Throwable error) { + String localizedMessage = getLocalizedMessage(error); + + this.logError(id, username, localizedMessage); + } + + protected void logError(AbstractResilientLongIdEntity id, String username, String message) { + this.log(ResilientLogLevel.ERROR, id, username, message); + } + + protected void logInfo(AbstractResilientLongIdEntity id, String username, String message) { + this.log(ResilientLogLevel.INFO, id, username, message); + } + + private void log(ResilientLogLevel level, AbstractResilientLongIdEntity id, String username, String message) { + ResilientLog log = new ResilientLog(); + + //@formatter:off + log + .ownerId(id.getId()) + .level(level) + .creationDate(Instant.now()) + .creationUsername(username) + .logMessage(message); + //@formatter:on + + this.resilientLogService.saveWithNewTransaction(log); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/AbstractActivityDomain.java b/src/main/java/com/oguerreiro/resilient/activity/AbstractActivityDomain.java new file mode 100644 index 0000000..8c7aded --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/AbstractActivityDomain.java @@ -0,0 +1,31 @@ +package com.oguerreiro.resilient.activity; + +import org.hibernate.annotations.Parameter; +import org.hibernate.annotations.Type; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.domain.AbstractResilientLongIdEntity; +import com.oguerreiro.resilient.domain.usertype.ActivityProgressUserType; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class AbstractActivityDomain> + extends AbstractResilientLongIdEntity implements ActivityDomain { + private static final long serialVersionUID = 1L; + + @Type(value = ActivityProgressUserType.class, parameters = { + @Parameter(name = "activityDomainClass", value = "com.oguerreiro.resilient.domain.Period") }) + @Column(name = "id", insertable = false, updatable = false) + private String activityProgressKey; + + public String getActivityProgressKey() { + return activityProgressKey; + } + + public void setActivityProgressKey(String activityProgressKey) { + this.activityProgressKey = activityProgressKey; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/AbstractReaderThreadSafeContext.java b/src/main/java/com/oguerreiro/resilient/activity/AbstractReaderThreadSafeContext.java new file mode 100644 index 0000000..57973bd --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/AbstractReaderThreadSafeContext.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.activity; + +import java.util.Iterator; + +/** + * + * @param is the reader type class + */ +public abstract class AbstractReaderThreadSafeContext { + private final Iterator reader; + + protected AbstractReaderThreadSafeContext(Iterator reader) { + this.reader = reader; + } + + public synchronized T next() { + if (this.reader.hasNext()) + return this.reader.next(); + + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/AbstractSelfEnabler.java b/src/main/java/com/oguerreiro/resilient/activity/AbstractSelfEnabler.java new file mode 100644 index 0000000..13f26a3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/AbstractSelfEnabler.java @@ -0,0 +1,50 @@ +package com.oguerreiro.resilient.activity; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +/** + * @param the interface that defines the SELF bean. Will be used to automatically infer the bean and supply the + * self() bean reference + */ +public abstract class AbstractSelfEnabler { + private SELF self; + + @Autowired + private ApplicationContext applicationContext; + + /** + * Use this, to make calls within the Task instance to another method in the this same Task, forcing the call to be + * done to the proxy. + * Specifically, this should be used so the annotations @Async and @Transaction work between calls of method sin the + * same beans instance. + * ATTENTION: The bean MUST be defined by an interface. If not, it doesn't work. + * + * @return + */ + protected SELF self() { + if (this.self == null) { + this.self = this.getBean(getGenericTypeClass()); + } + return this.self; + } + + private B getBean(Class beanClass) { + return applicationContext.getBean(beanClass); + } + + /** + * At runtime retrieves the actual Interface passed to the Generic + * + * @return + */ + @SuppressWarnings("unchecked") + private Class getGenericTypeClass() { + Type type = getClass().getGenericSuperclass(); + ParameterizedType parameterizedType = (ParameterizedType) type; + return (Class) parameterizedType.getActualTypeArguments()[2]; // Get the third Generica arg + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/AbstractTask.java b/src/main/java/com/oguerreiro/resilient/activity/AbstractTask.java new file mode 100644 index 0000000..0692b20 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/AbstractTask.java @@ -0,0 +1,118 @@ +package com.oguerreiro.resilient.activity; + +import java.time.Instant; +import java.util.Locale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.i18n.LocaleContextHolder; + +import com.oguerreiro.resilient.domain.AbstractResilientLongIdEntity; +import com.oguerreiro.resilient.domain.ResilientLog; +import com.oguerreiro.resilient.domain.enumeration.ResilientLogLevel; +import com.oguerreiro.resilient.error.ResilientException; +import com.oguerreiro.resilient.error.ResilientRuntimeException; +import com.oguerreiro.resilient.service.ResilientLogService; + +public abstract class AbstractTask> + extends AbstractSelfEnabler implements Task { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private MessageSource messageSource; + + @Autowired + private ResilientLogService resilientLogService; + + protected AbstractTask() { + } + + /** + * Sugar method to get a bean from {@link ApplicationContext}. Use this to get a a bean of scope + * {@link ConfigurableBeanFactory#SCOPE_PROTOTYPE SCOPE_PROTOTYPE}. + * Any other scope, just inject the bean into the class + * + * @param + * @param beanClass + * @return + */ + protected B getBean(Class beanClass) { + return applicationContext.getBean(beanClass); + } + + public final String getLocalizedMessage(Throwable exception) { + String localizedMessage = ""; + Locale locale = LocaleContextHolder.getLocale(); + + if (exception instanceof ResilientException) { + //This is a translatable Resilient message + ResilientException resilientException = (ResilientException) exception; + try { + localizedMessage = messageSource.getMessage(resilientException.getMessageCode(), + resilientException.getMessageArgs(), locale); + } catch (NoSuchMessageException ex) { + // Fall back to exception LocalizedMessage. + localizedMessage = exception.getLocalizedMessage(); + } + + if (localizedMessage == null) { + localizedMessage = resilientException.getMessageCode(); + } + } else if (exception instanceof ResilientRuntimeException) { + //This is a translatable Resilient message + ResilientRuntimeException resilientException = (ResilientRuntimeException) exception; + try { + localizedMessage = messageSource.getMessage(resilientException.getMessageCode(), + resilientException.getMessageArgs(), locale); + } catch (NoSuchMessageException ex) { + // Fall back to exception LocalizedMessage. + localizedMessage = exception.getLocalizedMessage(); + } + + if (localizedMessage == null) { + localizedMessage = resilientException.getMessageCode(); + } + } else { + localizedMessage = exception.getLocalizedMessage(); + } + + return localizedMessage; + } + + protected void logError(AbstractResilientLongIdEntity id, String username, Throwable error) { + String localizedMessage = getLocalizedMessage(error); + + this.logError(id, username, localizedMessage); + } + + protected void logError(AbstractResilientLongIdEntity id, String username, String message) { + this.log(ResilientLogLevel.ERROR, id, username, message); + } + + protected void logInfo(AbstractResilientLongIdEntity id, String username, String message) { + this.log(ResilientLogLevel.INFO, id, username, message); + } + + private void log(ResilientLogLevel level, AbstractResilientLongIdEntity id, String username, String message) { + ResilientLog log = new ResilientLog(); + + //@formatter:off + log + .ownerId(id.getId()) + .level(level) + .creationDate(Instant.now()) + .creationUsername(username) + .logMessage(message); + //@formatter:on + + this.resilientLogService.saveWithNewTransaction(log); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/Activity.java b/src/main/java/com/oguerreiro/resilient/activity/Activity.java new file mode 100644 index 0000000..53b2345 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/Activity.java @@ -0,0 +1,18 @@ +package com.oguerreiro.resilient.activity; + +import java.util.List; + +/** + * Just an interface to identify the Activity concept + */ +public interface Activity { + // Selection strategy definition + Class appliesToActivityDomainClass(); + List appliesToStates(); + + // Activity identity + String getActivityKey(); + + // Activity methods + AR doActivity(String activityDomainClass, Long activityDomainId); +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/ActivityDomain.java b/src/main/java/com/oguerreiro/resilient/activity/ActivityDomain.java new file mode 100644 index 0000000..ddd5cc6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/ActivityDomain.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.activity; + +public interface ActivityDomain { + public Long getId(); + + public String getActivityProgressKey(); + + default String getActivityDomainClass() { + return this.getClass().getName(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/ActivityGuide.java b/src/main/java/com/oguerreiro/resilient/activity/ActivityGuide.java new file mode 100644 index 0000000..72fc7b9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/ActivityGuide.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.activity; + +import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor; + +/** + * Interface implementations, should have all the properties needed to perform the activity + */ +public interface ActivityGuide { + /** + * The username of the current signed in user, when the activity was launched. Usually obtained by {@code SecurityUtils.getCurrentUserLogin()} + */ + public String getUsername(); + + /** + * A reference to the ActivityProgressDescriptor registered in the ActivityProgressRegistry + * @return + */ + public ActivityProgressDescriptor getActivityProgressDescriptor(); +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/ActivityRegistry.java b/src/main/java/com/oguerreiro/resilient/activity/ActivityRegistry.java new file mode 100644 index 0000000..49d0ae6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/ActivityRegistry.java @@ -0,0 +1,90 @@ +package com.oguerreiro.resilient.activity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ActivityRegistry { + /* Activities by state */ + private final Map> activitiesByState = new HashMap>(); + private final Map activitiesByKey = new HashMap(); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Autowired + public ActivityRegistry(List activityList) { + for (Activity activity : activityList) { + if (activitiesByKey.containsKey(activity.getActivityKey())) { + throw new RuntimeException("Duplicated Activity key found : <" + activity.getActivityKey() + ">. Activity key must be unique."); + } + + activitiesByKey.put(activity.getActivityKey(), activity); + + String className = activity.appliesToActivityDomainClass().getName(); + + activity.appliesToStates().forEach((state) -> { + String key = className + "@" + state; + List activitiesForState = activitiesByState.get(key); + if (activitiesForState == null) { + activitiesForState = new ArrayList(); + activitiesByState.put(className + "@" + state , activitiesForState); + } + activitiesForState.add(activity.getActivityKey()); + }); + + } + } + + @SuppressWarnings("rawtypes") + public Map findActivitiesFor(String activityDomainClass, String currentState) { + String key = activityDomainClass + "@" + currentState; + List activitiesForState = activitiesByState.get(key); + if (activitiesForState == null) + return Collections.emptyMap(); + + Map activities = new HashMap(); + for (String activityKey : activitiesForState) { + activities.put(activityKey, activitiesByKey.get(activityKey)); + } + return activities; + } + + /** + * Get {@link Activity} by the key, when it was registered + * + * @param activityKey + * @return + */ + public Activity getActivity(String activityKey) { + Activity activity= activitiesByKey.get(activityKey); + + if (activity==null) { + // TODO : throw exception : ActivityNotFound for ActivityDomainClass and UUID + } + + return activitiesByKey.get(activityKey); + } + + /** + * Get {@link Activity} by the UUID, when it was registered. But also checks if it apoplies to the activityDomainClass. + * + * @param activityUUID + * @return + */ + public Activity getActivity(String activityUUID, String activityDomainClass) { + Activity activity= this.getActivity(activityUUID); + + if (activity.appliesToActivityDomainClass().getName().equals(activityDomainClass)) { + return activity; + } + + //TODO : throw exception : ActivityNotFound for ActivityDomainClass and UUID + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/ActivityResult.java b/src/main/java/com/oguerreiro/resilient/activity/ActivityResult.java new file mode 100644 index 0000000..fcb332b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/ActivityResult.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.activity; + +/** + * Interface implementations, should have all the properties needed to perform the activity + */ +public interface ActivityResult { + WorkResultEnum getWorkResult(); +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/GenericGuide.java b/src/main/java/com/oguerreiro/resilient/activity/GenericGuide.java new file mode 100644 index 0000000..1cf8c82 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/GenericGuide.java @@ -0,0 +1,42 @@ +package com.oguerreiro.resilient.activity; + +import java.util.HashMap; +import java.util.Map; + +import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor; + +public class GenericGuide implements ActivityGuide, TaskGuide { + private final String username; + private final ActivityProgressDescriptor activityProgressDescriptor; + private AD activityDomain; + public Map otherValues = new HashMap(); + + protected GenericGuide(ActivityProgressDescriptor activityProgressDescriptor, AD activityDomain, String username) { + this.username = username; + this.activityProgressDescriptor = activityProgressDescriptor; + this.activityDomain = activityDomain; + } + + public static GenericGuide create( + ActivityProgressDescriptor activityProgressDescriptor, AD activityDomain, String username) { + return new GenericGuide(activityProgressDescriptor, activityDomain, username); + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public ActivityProgressDescriptor getActivityProgressDescriptor() { + return this.activityProgressDescriptor; + } + + public AD getActivityDomain() { + return this.activityDomain; + } + + public void setActivityDomain(AD activityDomain) { + this.activityDomain = activityDomain; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/GenericResult.java b/src/main/java/com/oguerreiro/resilient/activity/GenericResult.java new file mode 100644 index 0000000..838fdc7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/GenericResult.java @@ -0,0 +1,50 @@ +package com.oguerreiro.resilient.activity; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.spi.LoggingEventBuilder; + +public class GenericResult implements ActivityResult, TaskResult { + private static final String LOG_IDENT = " "; + private WorkResultEnum result; + private List errors = new ArrayList(); + + public GenericResult(WorkResultEnum result) { + this.result=result; + } + + public static GenericResult create(WorkResultEnum result) { + return new GenericResult(result); + } + + @Override + public WorkResultEnum getWorkResult() { + return result; + } + + public void addError(String error) { + this.errors.add(error); + } + + public GenericResult errors(List errors) { + this.addErrors(errors); + return this; + } + + public void addErrors(List errors) { + this.errors.addAll(errors); + } + + @Override + public List getErrors() { + return this.errors; + } + + public void logErrrors(LoggingEventBuilder log) { + log.log("Errors collected in Activity/Task processing: "); + for (String error : this.errors) { + log.log(LOG_IDENT + error); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/Task.java b/src/main/java/com/oguerreiro/resilient/activity/Task.java new file mode 100644 index 0000000..dff6553 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/Task.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.activity; + +/** + * Just an interface to identify the Activity concept + */ +public interface Task { + TR doTask(TG guide); +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/TaskGuide.java b/src/main/java/com/oguerreiro/resilient/activity/TaskGuide.java new file mode 100644 index 0000000..2381eec --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/TaskGuide.java @@ -0,0 +1,7 @@ +package com.oguerreiro.resilient.activity; + +/** + * Interface implementations, should have all the properties needed to perform the task + */ +public interface TaskGuide { +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/TaskResult.java b/src/main/java/com/oguerreiro/resilient/activity/TaskResult.java new file mode 100644 index 0000000..f531b68 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/TaskResult.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.activity; + +import java.util.List; + +import org.slf4j.spi.LoggingEventBuilder; + +/** + * Interface implementations, should have all the properties needed to perform the activity + */ +public interface TaskResult { + WorkResultEnum getWorkResult(); + List getErrors(); + void logErrrors(LoggingEventBuilder log); +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/WorkResultEnum.java b/src/main/java/com/oguerreiro/resilient/activity/WorkResultEnum.java new file mode 100644 index 0000000..4c1bbe6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/WorkResultEnum.java @@ -0,0 +1,6 @@ +package com.oguerreiro.resilient.activity; + +public enum WorkResultEnum { + COMPLETED, + FAILED +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/registry/ActivityProgressDescriptor.java b/src/main/java/com/oguerreiro/resilient/activity/registry/ActivityProgressDescriptor.java new file mode 100644 index 0000000..8f708ee --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/registry/ActivityProgressDescriptor.java @@ -0,0 +1,67 @@ +package com.oguerreiro.resilient.activity.registry; + +import java.time.Instant; + +import com.oguerreiro.resilient.security.SecurityUtils; + +public class ActivityProgressDescriptor { + public static final String INITIAL_PROGRESS_TASK = "INITIALIZING"; + + private Instant start; + private Instant end; + private String username; + private Instant lastTick; // Just an evolution time based, even if there's no progress info. at least, we + // know the process is still alive and working. + + private Integer progressPercentage; + private String progressTask; + + public ActivityProgressDescriptor(Instant start, String username) { + this.start = start; + + // If username is null, use the currently logged user + if (username == null || username.trim().length() == 0) { + username = SecurityUtils.getCurrentUserLogin().orElseThrow(); + } + this.username = username; + + // Initialize percentage + this.progressPercentage = Integer.valueOf(0); + this.progressTask = INITIAL_PROGRESS_TASK; + } + + // Properties + public Instant getStart() { + return start; + } + + public Instant getEnd() { + return end; + } + + public String getUsername() { + return username; + } + + public Instant getLastTick() { + return lastTick; + } + + // Methods + public void tick() { + this.lastTick = Instant.now(); + } + + public Integer getProgressPercentage() { + return this.progressPercentage; + } + + public String getProgressTask() { + return this.progressTask; + } + + public void updateProgress(Integer progress, String task) { + this.progressPercentage = progress; + this.progressTask = task; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/activity/registry/ActivityProgressRegistry.java b/src/main/java/com/oguerreiro/resilient/activity/registry/ActivityProgressRegistry.java new file mode 100644 index 0000000..70e16f7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/activity/registry/ActivityProgressRegistry.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.activity.registry; + +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.activity.ActivityDomain; + +/** + * Class that keeps track of all Activities/Tasks in execution. And also have + * info about progress + */ +@Component +public class ActivityProgressRegistry { + Map registry = new ConcurrentHashMap(); + + public ActivityProgressDescriptor progressOf(String activityRegistryKey) { + return registry.get(activityRegistryKey); + } + + public ActivityProgressDescriptor progressAdd(ActivityDomain entity) { + ActivityProgressDescriptor pd = new ActivityProgressDescriptor(Instant.now(), ""); + registry.put(entity.getActivityDomainClass() + "@" + entity.getId(), pd); + + return pd; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/aop/logging/LoggingAspect.java b/src/main/java/com/oguerreiro/resilient/aop/logging/LoggingAspect.java new file mode 100644 index 0000000..678ff7d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/aop/logging/LoggingAspect.java @@ -0,0 +1,113 @@ +package com.oguerreiro.resilient.aop.logging; + +import java.util.Arrays; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import tech.jhipster.config.JHipsterConstants; + +/** + * Aspect for logging execution of service and repository Spring components. + * + * By default, it only runs with the "dev" profile. + */ +@Aspect +public class LoggingAspect { + + private final Environment env; + + public LoggingAspect(Environment env) { + this.env = env; + } + + /** + * Pointcut that matches all repositories, services and Web REST endpoints. + */ + @Pointcut( + "within(@org.springframework.stereotype.Repository *)" + + " || within(@org.springframework.stereotype.Service *)" + + " || within(@org.springframework.web.bind.annotation.RestController *)" + ) + public void springBeanPointcut() { + // Method is empty as this is just a Pointcut, the implementations are in the advices. + } + + /** + * Pointcut that matches all Spring beans in the application's main packages. + */ + @Pointcut( + "within(com.oguerreiro.resilient.repository..*)" + + " || within(com.oguerreiro.resilient.service..*)" + + " || within(com.oguerreiro.resilient.web.rest..*)" + ) + public void applicationPackagePointcut() { + // Method is empty as this is just a Pointcut, the implementations are in the advices. + } + + /** + * Retrieves the {@link Logger} associated to the given {@link JoinPoint}. + * + * @param joinPoint join point we want the logger for. + * @return {@link Logger} associated to the given {@link JoinPoint}. + */ + private Logger logger(JoinPoint joinPoint) { + return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringTypeName()); + } + + /** + * Advice that logs methods throwing exceptions. + * + * @param joinPoint join point for advice. + * @param e exception. + */ + @AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e") + public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { + if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) { + logger(joinPoint).error( + "Exception in {}() with cause = '{}' and exception = '{}'", + joinPoint.getSignature().getName(), + e.getCause() != null ? e.getCause() : "NULL", + e.getMessage(), + e + ); + } else { + logger(joinPoint).error( + "Exception in {}() with cause = {}", + joinPoint.getSignature().getName(), + e.getCause() != null ? String.valueOf(e.getCause()) : "NULL" + ); + } + } + + /** + * Advice that logs when a method is entered and exited. + * + * @param joinPoint join point for advice. + * @return result. + * @throws Throwable throws {@link IllegalArgumentException}. + */ + @Around("applicationPackagePointcut() && springBeanPointcut()") + public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { + Logger log = logger(joinPoint); + if (log.isDebugEnabled()) { + log.debug("Enter: {}() with argument[s] = {}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); + } + try { + Object result = joinPoint.proceed(); + if (log.isDebugEnabled()) { + log.debug("Exit: {}() with result = {}", joinPoint.getSignature().getName(), result); + } + return result; + } catch (IllegalArgumentException e) { + log.error("Illegal argument: {} in {}()", Arrays.toString(joinPoint.getArgs()), joinPoint.getSignature().getName()); + throw e; + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/aop/logging/package-info.java b/src/main/java/com/oguerreiro/resilient/aop/logging/package-info.java new file mode 100644 index 0000000..dad82ff --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/aop/logging/package-info.java @@ -0,0 +1,4 @@ +/** + * Logging aspect. + */ +package com.oguerreiro.resilient.aop.logging; diff --git a/src/main/java/com/oguerreiro/resilient/config/ApplicationProperties.java b/src/main/java/com/oguerreiro/resilient/config/ApplicationProperties.java new file mode 100644 index 0000000..25f8346 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/ApplicationProperties.java @@ -0,0 +1,37 @@ +package com.oguerreiro.resilient.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties specific to Resilient. + *

+ * Properties are configured in the {@code application.yml} file. + * See {@link tech.jhipster.config.JHipsterProperties} for a good example. + */ +@ConfigurationProperties(prefix = "application", ignoreUnknownFields = false) +public class ApplicationProperties { + + private final Liquibase liquibase = new Liquibase(); + + // jhipster-needle-application-properties-property + + public Liquibase getLiquibase() { + return liquibase; + } + + // jhipster-needle-application-properties-property-getter + + public static class Liquibase { + + private Boolean asyncStart; + + public Boolean getAsyncStart() { + return asyncStart; + } + + public void setAsyncStart(Boolean asyncStart) { + this.asyncStart = asyncStart; + } + } + // jhipster-needle-application-properties-property-class +} diff --git a/src/main/java/com/oguerreiro/resilient/config/AsyncConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/AsyncConfiguration.java new file mode 100644 index 0000000..74b18b6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/AsyncConfiguration.java @@ -0,0 +1,48 @@ +package com.oguerreiro.resilient.config; + +import java.util.concurrent.Executor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.boot.autoconfigure.task.TaskExecutionProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import tech.jhipster.async.ExceptionHandlingAsyncTaskExecutor; + +@Configuration +@EnableAsync +@EnableScheduling +// @Profile("!testdev & !testprod") +public class AsyncConfiguration implements AsyncConfigurer { + + private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class); + + private final TaskExecutionProperties taskExecutionProperties; + + public AsyncConfiguration(TaskExecutionProperties taskExecutionProperties) { + this.taskExecutionProperties = taskExecutionProperties; + } + + @Override + @Bean(name = "taskExecutor") + public Executor getAsyncExecutor() { + log.debug("Creating Async Task Executor"); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize()); + executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize()); + executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity()); + executor.setThreadNamePrefix(taskExecutionProperties.getThreadNamePrefix()); + return new ExceptionHandlingAsyncTaskExecutor(executor); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/CRLFLogConverter.java b/src/main/java/com/oguerreiro/resilient/config/CRLFLogConverter.java new file mode 100644 index 0000000..0f177b6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/CRLFLogConverter.java @@ -0,0 +1,69 @@ +package com.oguerreiro.resilient.config; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.CompositeConverter; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import org.springframework.boot.ansi.AnsiColor; +import org.springframework.boot.ansi.AnsiElement; +import org.springframework.boot.ansi.AnsiOutput; +import org.springframework.boot.ansi.AnsiStyle; + +/** + * Log filter to prevent attackers from forging log entries by submitting input containing CRLF characters. + * CRLF characters are replaced with a red colored _ character. + * + * @see Log Forging Description + * @see JHipster issue + */ +public class CRLFLogConverter extends CompositeConverter { + + public static final Marker CRLF_SAFE_MARKER = MarkerFactory.getMarker("CRLF_SAFE"); + + private static final String[] SAFE_LOGGERS = { + "org.hibernate", + "org.springframework.boot.autoconfigure", + "org.springframework.boot.diagnostics", + }; + private static final Map ELEMENTS; + + static { + Map ansiElements = new HashMap<>(); + ansiElements.put("faint", AnsiStyle.FAINT); + ansiElements.put("red", AnsiColor.RED); + ansiElements.put("green", AnsiColor.GREEN); + ansiElements.put("yellow", AnsiColor.YELLOW); + ansiElements.put("blue", AnsiColor.BLUE); + ansiElements.put("magenta", AnsiColor.MAGENTA); + ansiElements.put("cyan", AnsiColor.CYAN); + ELEMENTS = Collections.unmodifiableMap(ansiElements); + } + + @Override + protected String transform(ILoggingEvent event, String in) { + AnsiElement element = ELEMENTS.get(getFirstOption()); + List markers = event.getMarkerList(); + if ((markers != null && !markers.isEmpty() && markers.get(0).contains(CRLF_SAFE_MARKER)) || isLoggerSafe(event)) { + return in; + } + String replacement = element == null ? "_" : toAnsiString("_", element); + return in.replaceAll("[\n\r\t]", replacement); + } + + protected boolean isLoggerSafe(ILoggingEvent event) { + for (String safeLogger : SAFE_LOGGERS) { + if (event.getLoggerName().startsWith(safeLogger)) { + return true; + } + } + return false; + } + + protected String toAnsiString(String in, AnsiElement element) { + return AnsiOutput.toString(element, in); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/CacheConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/CacheConfiguration.java new file mode 100644 index 0000000..38db9ca --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/CacheConfiguration.java @@ -0,0 +1,131 @@ +package com.oguerreiro.resilient.config; + +import java.time.Duration; +import org.ehcache.config.builders.*; +import org.ehcache.jsr107.Eh107Configuration; +import org.hibernate.cache.jcache.ConfigSettings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.GitProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.*; +import tech.jhipster.config.JHipsterProperties; +import tech.jhipster.config.cache.PrefixedKeyGenerator; + +@Configuration +@EnableCaching +public class CacheConfiguration { + + private GitProperties gitProperties; + private BuildProperties buildProperties; + private final javax.cache.configuration.Configuration jcacheConfiguration; + + public CacheConfiguration(JHipsterProperties jHipsterProperties) { + JHipsterProperties.Cache.Ehcache ehcache = jHipsterProperties.getCache().getEhcache(); + + jcacheConfiguration = Eh107Configuration.fromEhcacheCacheConfiguration( + CacheConfigurationBuilder.newCacheConfigurationBuilder( + Object.class, + Object.class, + ResourcePoolsBuilder.heap(ehcache.getMaxEntries()) + ) + .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(ehcache.getTimeToLiveSeconds()))) + .build() + ); + } + + @Bean + public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(javax.cache.CacheManager cacheManager) { + return hibernateProperties -> hibernateProperties.put(ConfigSettings.CACHE_MANAGER, cacheManager); + } + + @Bean + public JCacheManagerCustomizer cacheManagerCustomizer() { + return cm -> { + createCache(cm, com.oguerreiro.resilient.repository.UserRepository.USERS_BY_LOGIN_CACHE); + createCache(cm, com.oguerreiro.resilient.repository.UserRepository.USERS_BY_EMAIL_CACHE); + createCache(cm, com.oguerreiro.resilient.domain.User.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.Authority.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.User.class.getName() + ".authorities"); + createCache(cm, com.oguerreiro.resilient.domain.PersistentToken.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.User.class.getName() + ".persistentTokens"); + createCache(cm, com.oguerreiro.resilient.domain.OrganizationType.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.OrganizationType.class.getName() + ".organizations"); + createCache(cm, com.oguerreiro.resilient.domain.OrganizationType.class.getName() + ".metadataProperties"); + createCache(cm, com.oguerreiro.resilient.domain.Organization.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.Organization.class.getName() + ".children"); + createCache(cm, com.oguerreiro.resilient.domain.Organization.class.getName() + ".inputData"); + createCache(cm, com.oguerreiro.resilient.domain.Organization.class.getName() + ".inputDataUploads"); + createCache(cm, com.oguerreiro.resilient.domain.MetadataProperty.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.MetadataProperty.class.getName() + ".organizationTypes"); + createCache(cm, com.oguerreiro.resilient.domain.MetadataValue.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.UnitType.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.UnitType.class.getName() + ".units"); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName() + ".fromUnits"); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName() + ".toUnits"); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName() + ".baseUnits"); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName() + ".sourceUnits"); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName() + ".inputData"); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName() + ".outputData"); + createCache(cm, com.oguerreiro.resilient.domain.Unit.class.getName() + ".variableUnits"); + createCache(cm, com.oguerreiro.resilient.domain.UnitConverter.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.VariableScope.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.VariableScope.class.getName() + ".variableCategories"); + createCache(cm, com.oguerreiro.resilient.domain.VariableScope.class.getName() + ".variables"); + createCache(cm, com.oguerreiro.resilient.domain.VariableCategory.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.VariableCategory.class.getName() + ".variables"); + createCache(cm, com.oguerreiro.resilient.domain.Variable.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.Variable.class.getName() + ".variableClasses"); + createCache(cm, com.oguerreiro.resilient.domain.Variable.class.getName() + ".inputData"); + createCache(cm, com.oguerreiro.resilient.domain.Variable.class.getName() + ".outputData"); + createCache(cm, com.oguerreiro.resilient.domain.Variable.class.getName() + ".variableUnits"); + createCache(cm, com.oguerreiro.resilient.domain.VariableUnits.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.VariableClass.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.Period.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.Period.class.getName() + ".periodVersions"); + createCache(cm, com.oguerreiro.resilient.domain.Period.class.getName() + ".inputData"); + createCache(cm, com.oguerreiro.resilient.domain.Period.class.getName() + ".inputDataUploads"); + createCache(cm, com.oguerreiro.resilient.domain.Period.class.getName() + ".outputData"); + createCache(cm, com.oguerreiro.resilient.domain.PeriodVersion.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.PeriodVersion.class.getName() + ".inputData"); + createCache(cm, com.oguerreiro.resilient.domain.PeriodVersion.class.getName() + ".inputDataUploads"); + createCache(cm, com.oguerreiro.resilient.domain.PeriodVersion.class.getName() + ".outputData"); + createCache(cm, com.oguerreiro.resilient.domain.InputData.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.InputData.class.getName() + ".inputData"); + createCache(cm, com.oguerreiro.resilient.domain.InputDataUpload.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.InputDataUpload.class.getName() + ".inputData"); + createCache(cm, com.oguerreiro.resilient.domain.InputDataUpload.class.getName() + ".inputDataUploadLogs"); + createCache(cm, com.oguerreiro.resilient.domain.InputDataUploadLog.class.getName()); + createCache(cm, com.oguerreiro.resilient.domain.OutputData.class.getName()); + // jhipster-needle-ehcache-add-entry + }; + } + + private void createCache(javax.cache.CacheManager cm, String cacheName) { + javax.cache.Cache cache = cm.getCache(cacheName); + if (cache != null) { + cache.clear(); + } else { + cm.createCache(cacheName, jcacheConfiguration); + } + } + + @Autowired(required = false) + public void setGitProperties(GitProperties gitProperties) { + this.gitProperties = gitProperties; + } + + @Autowired(required = false) + public void setBuildProperties(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } + + @Bean + public KeyGenerator keyGenerator() { + return new PrefixedKeyGenerator(this.gitProperties, this.buildProperties); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/CertificateLoader.java b/src/main/java/com/oguerreiro/resilient/config/CertificateLoader.java new file mode 100644 index 0000000..f8d2b9a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/CertificateLoader.java @@ -0,0 +1,53 @@ +package com.oguerreiro.resilient.config; + +import java.io.FileInputStream; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +public class CertificateLoader { + + public static X509Certificate loadCertificate(String filePath) throws Exception { + CertificateFactory factory = CertificateFactory + .getInstance( + "X.509"); + try (FileInputStream fis = new FileInputStream(filePath)) { + return (X509Certificate) factory + .generateCertificate( + fis); + } + } + + public static PrivateKey loadPrivateKey(String filePath) throws Exception { + byte[] keyBytes = java.nio.file.Files + .readAllBytes( + java.nio.file.Paths + .get( + filePath)); + String keyPEM = new String(keyBytes) + .replace( + "-----BEGIN PRIVATE KEY-----", + "") + .replace( + "-----END PRIVATE KEY-----", + "") + .replaceAll( + "\\s", + ""); + byte[] decodedKey = Base64 + .getDecoder() + .decode( + keyPEM); + + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); + KeyFactory keyFactory = KeyFactory + .getInstance( + "RSA"); + return keyFactory + .generatePrivate( + keySpec); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/Constants.java b/src/main/java/com/oguerreiro/resilient/config/Constants.java new file mode 100644 index 0000000..2a8a75b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/Constants.java @@ -0,0 +1,15 @@ +package com.oguerreiro.resilient.config; + +/** + * Application constants. + */ +public final class Constants { + + // Regex for acceptable logins + public static final String LOGIN_REGEX = "^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$"; + + public static final String SYSTEM = "system"; + public static final String DEFAULT_LANGUAGE = "pt-pt"; + + private Constants() {} +} diff --git a/src/main/java/com/oguerreiro/resilient/config/DatabaseConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/DatabaseConfiguration.java new file mode 100644 index 0000000..8a8bf82 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/DatabaseConfiguration.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableJpaRepositories({ "com.oguerreiro.resilient.repository" }) +@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware") +@EnableTransactionManagement(proxyTargetClass = true) +public class DatabaseConfiguration {} diff --git a/src/main/java/com/oguerreiro/resilient/config/DateTimeFormatConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/DateTimeFormatConfiguration.java new file mode 100644 index 0000000..5b03323 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/DateTimeFormatConfiguration.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Configure the converters to use the ISO format for dates by default. + */ +@Configuration +public class DateTimeFormatConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/HttpAndHttpsUndertowConfig.java b/src/main/java/com/oguerreiro/resilient/config/HttpAndHttpsUndertowConfig.java new file mode 100644 index 0000000..936d165 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/HttpAndHttpsUndertowConfig.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HttpAndHttpsUndertowConfig implements WebServerFactoryCustomizer { + + @Value("${resilient.server.http.enabled:false}") + private Boolean httpEnabled; + + @Value("${resilient.server.http.port:8081}") + private Integer httpPort; + + @Override + public void customize(UndertowServletWebServerFactory factory) { + if (httpEnabled) { + factory.addBuilderCustomizers(builder -> builder.addHttpListener(httpPort, "0.0.0.0") // This enables HTTP on port 8081 + ); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/JacksonConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/JacksonConfiguration.java new file mode 100644 index 0000000..254a71c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/JacksonConfiguration.java @@ -0,0 +1,34 @@ +package com.oguerreiro.resilient.config; + +import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module; +import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module.Feature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfiguration { + + /** + * Support for Java date and time API. + * @return the corresponding Jackson module. + */ + @Bean + public JavaTimeModule javaTimeModule() { + return new JavaTimeModule(); + } + + @Bean + public Jdk8Module jdk8TimeModule() { + return new Jdk8Module(); + } + + /* + * Support for Hibernate types in Jackson. + */ + @Bean + public Hibernate6Module hibernate6Module() { + return new Hibernate6Module().configure(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS, true); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/LiquibaseConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/LiquibaseConfiguration.java new file mode 100644 index 0000000..c0b9577 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/LiquibaseConfiguration.java @@ -0,0 +1,81 @@ +package com.oguerreiro.resilient.config; + +import java.util.concurrent.Executor; +import javax.sql.DataSource; +import liquibase.integration.spring.SpringLiquibase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseDataSource; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.liquibase.SpringLiquibaseUtil; + +@Configuration +public class LiquibaseConfiguration { + + private final Logger log = LoggerFactory.getLogger(LiquibaseConfiguration.class); + + private final Environment env; + + public LiquibaseConfiguration(Environment env) { + this.env = env; + } + + @Value("${application.liquibase.async-start:true}") + private Boolean asyncStart; + + @Bean + public SpringLiquibase liquibase( + @Qualifier("taskExecutor") Executor executor, + LiquibaseProperties liquibaseProperties, + @LiquibaseDataSource ObjectProvider liquibaseDataSource, + ObjectProvider dataSource, + DataSourceProperties dataSourceProperties + ) { + SpringLiquibase liquibase; + if (Boolean.TRUE.equals(asyncStart)) { + liquibase = SpringLiquibaseUtil.createAsyncSpringLiquibase( + this.env, + executor, + liquibaseDataSource.getIfAvailable(), + liquibaseProperties, + dataSource.getIfUnique(), + dataSourceProperties + ); + } else { + liquibase = SpringLiquibaseUtil.createSpringLiquibase( + liquibaseDataSource.getIfAvailable(), + liquibaseProperties, + dataSource.getIfUnique(), + dataSourceProperties + ); + } + liquibase.setChangeLog("classpath:config/liquibase/master.xml"); + liquibase.setContexts(liquibaseProperties.getContexts()); + liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema()); + liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema()); + liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace()); + liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable()); + liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable()); + liquibase.setDropFirst(liquibaseProperties.isDropFirst()); + liquibase.setLabelFilter(liquibaseProperties.getLabelFilter()); + liquibase.setChangeLogParameters(liquibaseProperties.getParameters()); + liquibase.setRollbackFile(liquibaseProperties.getRollbackFile()); + liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate()); + if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_NO_LIQUIBASE))) { + liquibase.setShouldRun(false); + } else { + liquibase.setShouldRun(liquibaseProperties.isEnabled()); + log.debug("Configuring Liquibase"); + } + return liquibase; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/LocaleConfig.java b/src/main/java/com/oguerreiro/resilient/config/LocaleConfig.java new file mode 100644 index 0000000..30704ac --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/LocaleConfig.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.config; + +import java.util.Locale; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; + +@Configuration +public class LocaleConfig { + + @Bean + public LocaleResolver localeResolver() { + //SessionLocaleResolver slr = new SessionLocaleResolver(); + //slr.setDefaultLocale(new Locale("pt", "PT")); + //return slr; + LocaleContextHolder.setDefaultLocale(new Locale("pt", "PT")); + + AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver(); + resolver.setDefaultLocale(new Locale("pt", "PT")); + return resolver; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/LoggingAspectConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/LoggingAspectConfiguration.java new file mode 100644 index 0000000..3536180 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/LoggingAspectConfiguration.java @@ -0,0 +1,17 @@ +package com.oguerreiro.resilient.config; + +import com.oguerreiro.resilient.aop.logging.LoggingAspect; +import org.springframework.context.annotation.*; +import org.springframework.core.env.Environment; +import tech.jhipster.config.JHipsterConstants; + +@Configuration +@EnableAspectJAutoProxy +public class LoggingAspectConfiguration { + + @Bean + @Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + public LoggingAspect loggingAspect(Environment env) { + return new LoggingAspect(env); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/LoggingConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/LoggingConfiguration.java new file mode 100644 index 0000000..d0fd8a3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/LoggingConfiguration.java @@ -0,0 +1,47 @@ +package com.oguerreiro.resilient.config; + +import static tech.jhipster.config.logging.LoggingUtils.*; + +import ch.qos.logback.classic.LoggerContext; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.config.JHipsterProperties; + +/* + * Configures the console and Logstash log appenders from the app properties + */ +@Configuration +public class LoggingConfiguration { + + public LoggingConfiguration( + @Value("${spring.application.name}") String appName, + @Value("${server.port}") String serverPort, + JHipsterProperties jHipsterProperties, + ObjectMapper mapper + ) throws JsonProcessingException { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + + Map map = new HashMap<>(); + map.put("app_name", appName); + map.put("app_port", serverPort); + String customFields = mapper.writeValueAsString(map); + + JHipsterProperties.Logging loggingProperties = jHipsterProperties.getLogging(); + JHipsterProperties.Logging.Logstash logstashProperties = loggingProperties.getLogstash(); + + if (loggingProperties.isUseJsonFormat()) { + addJsonConsoleAppender(context, customFields); + } + if (logstashProperties.isEnabled()) { + addLogstashTcpSocketAppender(context, customFields, logstashProperties); + } + if (loggingProperties.isUseJsonFormat() || logstashProperties.isEnabled()) { + addContextListener(context, customFields, loggingProperties); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java new file mode 100644 index 0000000..4909e58 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java @@ -0,0 +1,334 @@ +package com.oguerreiro.resilient.config; + +/** + * Sometimes this import disappears. I think iIt happens when solving imports (Crtl+shft+O) + * import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + */ +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +import java.util.function.Supplier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.csrf.CsrfTokenRequestHandler; +import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.security.saml2.Saml2AuthenticationHandler; +import com.oguerreiro.resilient.security.saml2.Saml2ResponseLoggingFilter; +import com.oguerreiro.resilient.web.filter.SpaWebFilter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import tech.jhipster.config.JHipsterProperties; +import tech.jhipster.web.filter.CookieCsrfFilter; + +@Configuration +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfiguration { + + private final JHipsterProperties jHipsterProperties; + + private final RememberMeServices rememberMeServices; + + private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + + public SecurityConfiguration(RememberMeServices rememberMeServices, JHipsterProperties jHipsterProperties, + @Autowired(required = false) RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { + this.rememberMeServices = rememberMeServices; + this.jHipsterProperties = jHipsterProperties; + this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * For future knowledge in SAMLv2 config, its necessary to : + *

    + *
  • In application.yml add saml2 relyingPartyRegistration. SpringSecurity will automatically create and configure + * the bean {@link RelyingPartyRegistrationRepository relyingPartyRegistrationRepository}, used in saml2 + * configuration.
  • + *
  • Add in ResilientApp.main() the {@code Security.addProvider(new BouncyCastleProvider());}
  • + *
  • Escape the path /saml2 from Angular dispatcher to index.html, in + * SpaWebfilter.java: {@code !path.startsWith("/saml2")}
  • + *
  • Add {@link Saml2MetadataFilter}, AFTER csrf and BEFORE other filters in + * {@link SecurityConfiguration#filterChain()}: + *
      + *
    • Declare : + *
        + *
      • {@code OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver();}
      • + *
      • {@code Saml2MetadataFilter metadataFilter = new Saml2MetadataFilter(relyingPartyRegistrationRepository, metadataResolver);}
      • + *
      + * + *
    • + *
    • {@code .addFilterBefore(metadataFilter, BasicAuthenticationFilter.class)}
    • + *
    + *
  • + *
  • Add /saml2 paths to permitAll() in {@link SecurityConfiguration#filterChain()}: + *
      + *
    • {@code .requestMatchers(mvc.pattern("/saml2/service-provider-metadata/**")).permitAll()}
    • + *
    • {@code .requestMatchers(mvc.pattern("/saml2/authenticate/**")).permitAll()}
    • + *
    • {@code .requestMatchers(mvc.pattern("/saml2/**")).permitAll()}
    • + *
    + *
  • + *
+ * + * SecurityFilterChain, if SAML2 is configured, will be appended with: + * + *
+   * 
+    // SAML2 Login - Only if a relyingPartyRegistrationRepository is present
+    if (this.relyingPartyRegistrationRepository != null) {
+      OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver();
+      Saml2MetadataFilter metadataFilter = new Saml2MetadataFilter(relyingPartyRegistrationRepository, metadataResolver);
+      
+      http
+        .addFilterBefore(metadataFilter, BasicAuthenticationFilter.class)                          // For SAMLv2 metadata.
+        .addFilterBefore(new Saml2ResponseLoggingFilter(), Saml2WebSsoAuthenticationFilter.class)  // For SAML XML Response output logging
+        .saml2Login(
+            saml2 -> saml2
+              .relyingPartyRegistrationRepository(relyingPartyRegistrationRepository)
+              .successHandler(saml2AuthenticationHandler)
+              .failureHandler(saml2AuthenticationHandler)
+        )
+      ;
+    }
+  
+   * 
+   * 
+ * + * application.yml will look something like this: + * + *
+   * 
+  resilient:
+    security:
+      saml2:     # ADDED to support SAMLv2 authentication to IDP.
+                 # Metadata endpoint ${base-url}/saml2/service-provider-metadata/mock-idp
+      enabled: true
+      base-url: https://localhost:8443
+      success-url: http://localhost:4200/
+      failure-url: http://localhost:4200/
+        relyingparty:
+          registration:
+            mock-idp:
+              assertingparty:
+                entity-id: http://localhost:3000/saml/metadata
+                single-sign-on:
+                  url: http://localhost:3000/saml/sso
+                single-logout:
+                  url: http://localhost:3000/saml/slo
+                verification:
+                  credentials:
+                    - certificate-location: classpath:saml/idp-public.cert
+              signing:
+                credentials:
+                  - private-key-location: classpath:saml/private.key
+                    certificate-location: classpath:saml/public.cert
+   * 
+   * 
+ */ + //@formatter:off + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc, Saml2AuthenticationHandler saml2AuthenticationHandler) throws Exception { + http + .cors(withDefaults()) + .csrf( + csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler( + new SpaCsrfTokenRequestHandler())) + .addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class) + .addFilterAfter(new CookieCsrfFilter(), BasicAuthenticationFilter.class) + .headers( + headers -> + headers + .contentSecurityPolicy(csp -> csp.policyDirectives(jHipsterProperties.getSecurity().getContentSecurityPolicy())) + .frameOptions(FrameOptionsConfig::sameOrigin) + .referrerPolicy( + referrer -> + referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN) + ) + .permissionsPolicy( + permissions -> + permissions.policy( + "camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()" + ) + ) + ) + .authorizeHttpRequests( + authz -> + + // prettier-ignore + authz.requestMatchers( + mvc.pattern("/index.html"), + mvc.pattern("/*.js"), + mvc.pattern("/*.txt"), + mvc.pattern("/*.json"), + mvc.pattern("/*.map"), + mvc.pattern("/*.css")).permitAll() + .requestMatchers( + mvc.pattern("/*.ico"), + mvc.pattern("/*.png"), + mvc.pattern("/*.svg"), + mvc.pattern("/*.webapp")).permitAll() + .requestMatchers(mvc.pattern("/app/**")).permitAll() + .requestMatchers(mvc.pattern("/i18n/**")).permitAll() + .requestMatchers(mvc.pattern("/content/**")).permitAll() + .requestMatchers(mvc.pattern("/swagger-ui/**")).permitAll() + .requestMatchers(mvc.pattern("/saml2/service-provider-metadata/**")).permitAll() // ** This are specific for SAML2 authentication. + .requestMatchers(mvc.pattern("/saml2/authenticate/**")).permitAll() // ** If not active, probably It shouldn't be added. + .requestMatchers(mvc.pattern("/saml2/**")).permitAll() // ** I think its harmless to keep it always configured. + .requestMatchers(mvc.pattern("/api/authenticate")).permitAll() + /* .requestMatchers(mvc.pattern("/api/register")).permitAll() DISABLED REQUESTS FOR NEW USERS */ + .requestMatchers(mvc.pattern("/api/activate")).permitAll() + .requestMatchers(mvc.pattern("/api/account/reset-password/init")).permitAll() + .requestMatchers(mvc.pattern("/api/account/reset-password/finish")).permitAll() + .requestMatchers(mvc.pattern("/api/account/saml2-endpoint")).permitAll() + .requestMatchers(mvc.pattern("/api/home/page")).permitAll() + .requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN) + .requestMatchers(mvc.pattern("/api/**")).authenticated() + .requestMatchers(mvc.pattern("/v3/api-docs/**")).hasAuthority(AuthoritiesConstants.ADMIN) + .requestMatchers(mvc.pattern("/management/health")).permitAll() + .requestMatchers(mvc.pattern("/management/health/**")).permitAll() + .requestMatchers(mvc.pattern("/management/info")).permitAll() + /* .requestMatchers(mvc.pattern("/management/prometheus")).permitAll() //SECURED to ROLE_ADMIN */ + .requestMatchers(mvc.pattern("/management/prometheus")).hasAuthority(AuthoritiesConstants.ADMIN) + .requestMatchers(mvc.pattern("/management/**")).hasAuthority(AuthoritiesConstants.ADMIN) + ) + .rememberMe( + rememberMe -> + rememberMe.rememberMeServices(rememberMeServices) + .rememberMeParameter("remember-me") + .key(jHipsterProperties.getSecurity().getRememberMe().getKey()) + ) + .exceptionHandling( + exceptionHanding -> + exceptionHanding.defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new OrRequestMatcher(antMatcher("/api/**")) + ) + ) + .formLogin( + formLogin -> + formLogin.loginPage("/") + .loginProcessingUrl("/api/authentication") + .successHandler( + (request, response, authentication) -> + response.setStatus(HttpStatus.OK.value()) + ) + .failureHandler( + (request, response, exception) -> + response.setStatus(HttpStatus.UNAUTHORIZED.value()) + ) + .permitAll() + ) + .logout( + logout -> + logout.logoutUrl("/api/logout") + .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) + .permitAll() + ) + ; + + + // SAML2 Login - Only if a relyingPartyRegistrationRepository is present + if (this.relyingPartyRegistrationRepository != null) { + OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver(); + Saml2MetadataFilter metadataFilter = new Saml2MetadataFilter(relyingPartyRegistrationRepository, metadataResolver); + + /** + * For SAML its necessary to add in ResilientApp.main() the + * Security.addProvider(new BouncyCastleProvider()); + */ + http + .addFilterBefore(metadataFilter, BasicAuthenticationFilter.class) // For SAMLv2 metadata. + .addFilterBefore(new Saml2ResponseLoggingFilter(), Saml2WebSsoAuthenticationFilter.class) // For SAML XML Response output logging + .saml2Login( + saml2 -> saml2 + .relyingPartyRegistrationRepository(relyingPartyRegistrationRepository) + .successHandler(saml2AuthenticationHandler) + .failureHandler(saml2AuthenticationHandler) + ) + ; + } + + return http.build(); + } + //@formatter:on + + @Bean + MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { + return new MvcRequestMatcher.Builder(introspector); + } + + /** + * Custom CSRF handler to provide BREACH protection. + * + * @see Spring + * Security Documentation - Integrating with CSRF Protection + * @see JHipster - use customized + * SpaCsrfTokenRequestHandler to handle CSRF token + * @see CSRF protection not working with Spring Security 6 + */ + static final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler { + + private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler(); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, Supplier csrfToken) { + /* + * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of + * the CsrfToken when it is rendered in the response body. + */ + this.delegate.handle(request, response, csrfToken); + } + + @Override + public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { + /* + * If the request contains a request header, use CsrfTokenRequestAttributeHandler + * to resolve the CsrfToken. This applies when a single-page application includes + * the header value automatically, which was obtained via a cookie containing the + * raw CsrfToken. + */ + if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) { + return super.resolveCsrfTokenValue(request, csrfToken); + } + /* + * In all other cases (e.g. if the request contains a request parameter), use + * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies + * when a server-side rendered form includes the _csrf request parameter as a + * hidden input. + */ + return this.delegate.resolveCsrfTokenValue(request, csrfToken); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/SecurityResourcesConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/SecurityResourcesConfiguration.java new file mode 100644 index 0000000..1169de8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/SecurityResourcesConfiguration.java @@ -0,0 +1,63 @@ +package com.oguerreiro.resilient.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.type.filter.AnnotationTypeFilter; + +import com.oguerreiro.resilient.repository.security.SecurityResourceRepository; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; +import com.oguerreiro.resilient.security.resillient.SecurityResource; + +@Configuration +public class SecurityResourcesConfiguration { + @Bean + ApplicationRunner runner(SecurityResourceRepository securityResourceRepository) { + return args -> { + List> result = this.findAnnotatedClasses("com.oguerreiro.resilient"); + + result.forEach(c -> { + String code = c.getName(); + String name = c.getSimpleName(); + ResilientSecurityResourceConfig annotation = c.getAnnotation(ResilientSecurityResourceConfig.class); + + if (!annotation.code().equals(ResilientSecurityResourceConfig.DEFAULT_CODE)) { + code = annotation.code(); + } + + if (securityResourceRepository.findByCode(code).isEmpty()) { + //Insert new value + SecurityResource newSecurityResource = new SecurityResource().type(annotation.type()).code(code).name( + name).defaultCreatePermission(annotation.defaultCreatePermission()).defaultReadPermission( + annotation.defaultReadPermission()).defaultUpdatePermission( + annotation.defaultUpdatePermission()).defaultDeletePermission( + annotation.defaultDeletePermission()); + securityResourceRepository.save(newSecurityResource); + } + }); + }; + } + + public List> findAnnotatedClasses(String basePackage) { + List> classes = new ArrayList<>(); + + // Create a scanner and add a filter for the annotation + var scanner = new ClassPathScanningCandidateComponentProvider(false); + scanner.addIncludeFilter(new AnnotationTypeFilter(ResilientSecurityResourceConfig.class)); + + for (var beanDef : scanner.findCandidateComponents(basePackage)) { + try { + Class clazz = Class.forName(beanDef.getBeanClassName()); + classes.add(clazz); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Class not found: " + beanDef.getBeanClassName(), e); + } + } + + return classes; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/StaticResourcesWebConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/StaticResourcesWebConfiguration.java new file mode 100644 index 0000000..9fbf338 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/StaticResourcesWebConfiguration.java @@ -0,0 +1,47 @@ +package com.oguerreiro.resilient.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; + +@Configuration +@Profile({ JHipsterConstants.SPRING_PROFILE_PRODUCTION }) +public class StaticResourcesWebConfiguration implements WebMvcConfigurer { + + protected static final String[] RESOURCE_LOCATIONS = { "classpath:/static/", "classpath:/static/content/", "classpath:/static/i18n/" }; + protected static final String[] RESOURCE_PATHS = { "/*.js", "/*.css", "/*.svg", "/*.png", "*.ico", "/content/**", "/i18n/*" }; + + private final JHipsterProperties jhipsterProperties; + + public StaticResourcesWebConfiguration(JHipsterProperties jHipsterProperties) { + this.jhipsterProperties = jHipsterProperties; + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + ResourceHandlerRegistration resourceHandlerRegistration = appendResourceHandler(registry); + initializeResourceHandler(resourceHandlerRegistration); + } + + protected ResourceHandlerRegistration appendResourceHandler(ResourceHandlerRegistry registry) { + return registry.addResourceHandler(RESOURCE_PATHS); + } + + protected void initializeResourceHandler(ResourceHandlerRegistration resourceHandlerRegistration) { + resourceHandlerRegistration.addResourceLocations(RESOURCE_LOCATIONS).setCacheControl(getCacheControl()); + } + + protected CacheControl getCacheControl() { + return CacheControl.maxAge(getJHipsterHttpCacheProperty(), TimeUnit.DAYS).cachePublic(); + } + + private int getJHipsterHttpCacheProperty() { + return jhipsterProperties.getHttp().getCache().getTimeToLiveInDays(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/WebConfigurer.java b/src/main/java/com/oguerreiro/resilient/config/WebConfigurer.java new file mode 100644 index 0000000..8fa95e8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/WebConfigurer.java @@ -0,0 +1,100 @@ +package com.oguerreiro.resilient.config; + +import static java.net.URLDecoder.decode; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.server.WebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.CollectionUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import jakarta.servlet.ServletContext; +import tech.jhipster.config.JHipsterProperties; + +/** + * Configuration of web application with Servlet 3.0 APIs. + */ +@Configuration +public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer { + + private final Logger log = LoggerFactory.getLogger(WebConfigurer.class); + + private final Environment env; + + private final JHipsterProperties jHipsterProperties; + + public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) { + this.env = env; + this.jHipsterProperties = jHipsterProperties; + } + + @Override + public void onStartup(ServletContext servletContext) { + if (env.getActiveProfiles().length != 0) { + log.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles()); + } + + log.info("Web application fully configured"); + } + + /** + * Customize the Servlet engine: Mime types, the document root, the cache. + */ + @Override + public void customize(WebServerFactory server) { + // When running in an IDE or with ./mvnw spring-boot:run, set location of the static web assets. + setLocationForStaticAssets(server); + } + + private void setLocationForStaticAssets(WebServerFactory server) { + if (server instanceof ConfigurableServletWebServerFactory servletWebServer) { + File root; + String prefixPath = resolvePathPrefix(); + root = new File(prefixPath + "target/classes/static/"); + if (root.exists() && root.isDirectory()) { + servletWebServer.setDocumentRoot(root); + } + } + } + + /** + * Resolve path prefix to static resources. + */ + private String resolvePathPrefix() { + String fullExecutablePath = decode(this.getClass().getResource("").getPath(), StandardCharsets.UTF_8); + String rootPath = Paths.get(".").toUri().normalize().getPath(); + String extractedPath = fullExecutablePath.replace(rootPath, ""); + int extractionEndIndex = extractedPath.indexOf("target/"); + if (extractionEndIndex <= 0) { + return ""; + } + return extractedPath.substring(0, extractionEndIndex); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = jHipsterProperties.getCors(); + if (!CollectionUtils.isEmpty(config.getAllowedOrigins()) + || !CollectionUtils.isEmpty(config.getAllowedOriginPatterns())) { + log.debug("Registering CORS filter"); + source.registerCorsConfiguration("/api/**", config); + source.registerCorsConfiguration("/management/**", config); + source.registerCorsConfiguration("/v3/api-docs", config); + source.registerCorsConfiguration("/swagger-ui/**", config); + } + return new CorsFilter(source); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/config/package-info.java b/src/main/java/com/oguerreiro/resilient/config/package-info.java new file mode 100644 index 0000000..604792f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Application configuration. + */ +package com.oguerreiro.resilient.config; diff --git a/src/main/java/com/oguerreiro/resilient/domain/AbstractAuditingEntity.java b/src/main/java/com/oguerreiro/resilient/domain/AbstractAuditingEntity.java new file mode 100644 index 0000000..a5027da --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/AbstractAuditingEntity.java @@ -0,0 +1,78 @@ +package com.oguerreiro.resilient.domain; + +import java.io.Serializable; +import java.time.Instant; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; + +/** + * Base abstract class for entities which will hold definitions for created, last modified, created by, + * last modified by attributes. + */ +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@JsonIgnoreProperties(value = { "createdBy", "createdDate", "lastModifiedBy", "lastModifiedDate" }, allowGetters = true) +public abstract class AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + public abstract T getId(); + + @CreatedBy + @Column(name = "created_by", nullable = false, length = 50, updatable = false) + private String createdBy; + + @CreatedDate + @Column(name = "created_date", updatable = false) + private Instant createdDate = Instant.now(); + + @LastModifiedBy + @Column(name = "last_modified_by", length = 50) + private String lastModifiedBy; + + @LastModifiedDate + @Column(name = "last_modified_date") + private Instant lastModifiedDate = Instant.now(); + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Instant createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public Instant getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(Instant lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/AbstractResilientEntity.java b/src/main/java/com/oguerreiro/resilient/domain/AbstractResilientEntity.java new file mode 100644 index 0000000..d08a40c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/AbstractResilientEntity.java @@ -0,0 +1,59 @@ +package com.oguerreiro.resilient.domain; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; + +/** + * + * @param Entity Domain type class + * @param Entity Domain key type class + */ +@MappedSuperclass +public abstract class AbstractResilientEntity, ID> implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private ID id; + + /** + * Record version for concurrency control. + */ + @Version + @Column(name = "version") + private Integer version; + + protected abstract ID getId(); + + protected abstract T id(ID id); + + protected abstract void setId(ID id); + + public Integer getVersion() { + return this.version; + } + + // This is a safe cast, since its GUARANTEED that DOMAIN is always a subclass of AbstractEntity + @SuppressWarnings("unchecked") + public T version(Integer version) { + this.setVersion(version); + return (T) this; + } + + public void setVersion(Integer version) { + this.version = version; + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/AbstractResilientLongIdEntity.java b/src/main/java/com/oguerreiro/resilient/domain/AbstractResilientLongIdEntity.java new file mode 100644 index 0000000..5af40da --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/AbstractResilientLongIdEntity.java @@ -0,0 +1,63 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.Hibernate; + +import jakarta.persistence.Column; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import tech.jhipster.service.QueryService; + +/** + * Abstract Resilient Entity domain class with key type : {@link Long}
+ * All Entity domain's with Long primary key MUST extend {@link AbstractResilientLongIdEntity this}. + * + *

+ * This is needed because the hibernate generated classes (mappings) can't infer the {@link #id} property type from a + * Generic (my first solution). It was generated with type {@link Object}, which in turn, created a problem in the + * {@link QueryService#buildRangeSpecification}. + *

+ * + * @param Entity Domain type class + */ +@MappedSuperclass +public abstract class AbstractResilientLongIdEntity> + extends AbstractResilientEntity { + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Override + public Long getId() { + return this.id; + } + + @Override + @SuppressWarnings("unchecked") + public T id(Long id) { + this.setId(id); + return (T) this; // This is a safe cast, since its GUARANTEED that DOMAIN is always a subclass of AbstractEntity + } + + @Override + public void setId(Long id) { + this.id = id; + } + + // This is a safe cast, since its GUARANTEED that DOMAIN is always a subclass of AbstractEntity + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(Hibernate.getClass(o).isInstance(this))) { + return false; + } + return getId() != null && getId().equals(((T) o).getId()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/Authority.java b/src/main/java/com/oguerreiro/resilient/domain/Authority.java new file mode 100644 index 0000000..a535c19 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/Authority.java @@ -0,0 +1,95 @@ +package com.oguerreiro.resilient.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.io.Serializable; +import java.util.Objects; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.springframework.data.domain.Persistable; + +/** + * A Authority. + */ +@Entity +@Table(name = "jhi_authority") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@JsonIgnoreProperties(value = { "new", "id" }) +@SuppressWarnings("common-java:DuplicatedBlocks") +public class Authority implements Serializable, Persistable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Size(max = 50) + @Id + @Column(name = "name", length = 50, nullable = false) + private String name; + + @Transient + private boolean isPersisted; + + // jhipster-needle-entity-add-field - JHipster will add fields here + + public String getName() { + return this.name; + } + + public Authority name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + @PostLoad + @PostPersist + public void updateEntityState() { + this.setIsPersisted(); + } + + @Override + public String getId() { + return this.name; + } + + @Transient + @Override + public boolean isNew() { + return !this.isPersisted; + } + + public Authority setIsPersisted() { + this.isPersisted = true; + return this; + } + + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Authority)) { + return false; + } + return getName() != null && getName().equals(((Authority) o).getName()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getName()); + } + + // prettier-ignore + @Override + public String toString() { + return "Authority{" + + "name=" + getName() + + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/ContentPage.java b/src/main/java/com/oguerreiro/resilient/domain/ContentPage.java new file mode 100644 index 0000000..6923319 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/ContentPage.java @@ -0,0 +1,93 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.oguerreiro.resilient.domain.enumeration.ContentPageType; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * ContentPage + * Defines a HTML page content. + */ +@Entity +@Table(name = "content_page") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class ContentPage extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + @NotNull + @Column(name = "title", nullable = false) + private String title; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "slug", nullable = false, unique = true) + private ContentPageType slug; + + @NotNull + @Lob + @Column(name = "html_content", nullable = false) + private String content; + + public String getTitle() { + return title; + } + + public ContentPage title(String title) { + this.setTitle(title); + return this; + } + + public void setTitle(String title) { + this.title = title; + } + + public ContentPageType getSlug() { + return slug; + } + + public ContentPage slug(ContentPageType slug) { + this.setSlug(slug); + return this; + } + + public void setSlug(ContentPageType slug) { + this.slug = slug; + } + + public String getContent() { + return content; + } + + public ContentPage slug(String content) { + this.setContent(content); + return this; + } + + public void setContent(String content) { + this.content = content; + } + + //@formatter:off + @Override + public String toString() { + return "ContentPage{" + + "id=" + getId() + + ", title='" + getTitle() + "'" + + ", slug='" + getSlug() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/Dashboard.java b/src/main/java/com/oguerreiro/resilient/domain/Dashboard.java new file mode 100644 index 0000000..1c7f7a0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/Dashboard.java @@ -0,0 +1,118 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * Dashboard + * Defines a dashboard for presenting DashboardComponents. + */ +@Entity +@Table(name = "dashboard") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class Dashboard extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + public static final String DASHBOARD_EMISSIONS_CODE = "EMISSIONS"; + public static final String DASHBOARD_INDICATORS_CODE = "INDICATORS"; + + /** + * User defined code. Must be unique + * There's 2 internal codes: + * - EMISSIONS + * - INDICATORS + */ + @NotNull + @Size(min = 1) + @Column(name = "code", nullable = false, unique = true) + private String code; + + @NotNull + @Column(name = "name", nullable = false, unique = true) + private String name; + + @Column(name = "description") + private String description; + + public String getCode() { + return this.code; + } + + /** + * When set to true, means this is a specific dashboard to use in the app. It CAN'T be deleted. + */ + @Column(name = "is_internal") + private Boolean isInternal; + + public Dashboard code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public Dashboard name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public Dashboard description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getIsInternal() { + return this.isInternal; + } + + public Dashboard isInternal(Boolean isInternal) { + this.setIsInternal(isInternal); + return this; + } + + public void setIsInternal(Boolean isInternal) { + this.isInternal = isInternal; + } + +//@formatter:off + @Override + public String toString() { + return "DashboardComponent{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", isInternal='" + getIsInternal() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/DashboardComponent.java b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponent.java new file mode 100644 index 0000000..7ec869b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponent.java @@ -0,0 +1,272 @@ +package com.oguerreiro.resilient.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentChartType; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentType; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * DashboardComponent + * Defines a component for dashboard. Nothing more than a group of outputs (variables of output type) + */ +@Entity +@Table(name = "dashboard_component") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class DashboardComponent extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * User defined code. Must be unique + */ + @NotNull + @Size(min = 1) + @Column(name = "code", nullable = false, unique = true) + private String code; + + @NotNull + @Column(name = "name", nullable = false, unique = true) + private String name; + + @Column(name = "description") + private String description; + + @Column(name = "is_active") + private Boolean isActive; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "type") + private DashboardComponentType type; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "dashboard_view") + private DashboardComponentView view; + + @Enumerated(EnumType.STRING) + @Column(name = "chart_type") + private DashboardComponentChartType chartType; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "dashboardComponent", cascade = CascadeType.ALL, orphanRemoval = true) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "dashboardComponent" }, allowSetters = true) + @OrderBy("indexOrder ASC") + private List dashboardComponentDetails = new ArrayList<>(); + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "dashboardComponent", cascade = CascadeType.ALL, orphanRemoval = true) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "dashboardComponent" }, allowSetters = true) + private List dashboardComponentOrganizations = new ArrayList<>(); + + @NotNull + @Column(name = "is_exportable") + private Boolean isExportable; + + public String getCode() { + return this.code; + } + + public DashboardComponent code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public DashboardComponent name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public DashboardComponent description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getIsActive() { + return this.isActive; + } + + public DashboardComponent isActive(Boolean isActive) { + this.setIsActive(isActive); + return this; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public DashboardComponentType getType() { + return this.type; + } + + public DashboardComponent type(DashboardComponentType type) { + this.setType(type); + return this; + } + + public void setType(DashboardComponentType type) { + this.type = type; + } + + public DashboardComponentView getView() { + return this.view; + } + + public DashboardComponent view(DashboardComponentView view) { + this.setView(view); + return this; + } + + public void setView(DashboardComponentView view) { + this.view = view; + } + + public DashboardComponentChartType getChartType() { + return this.chartType; + } + + public DashboardComponent chartType(DashboardComponentChartType chartType) { + this.setChartType(chartType); + return this; + } + + public void setChartType(DashboardComponentChartType chartType) { + this.chartType = chartType; + } + + public List getDashboardComponentDetails() { + return this.dashboardComponentDetails; + } + + public void setDashboardComponentDetails(List dashboardComponentDetails) { + if (this.dashboardComponentDetails != null) { + this.dashboardComponentDetails.forEach((v) -> v.setDashboardComponent(null)); + } + if (dashboardComponentDetails != null) { + dashboardComponentDetails.forEach((v) -> v.setDashboardComponent(this)); + } + this.dashboardComponentDetails = dashboardComponentDetails; + } + + public DashboardComponent dashboardComponentDetails(List dashboardComponentDetails) { + this.setDashboardComponentDetails(dashboardComponentDetails); + return this; + } + + public DashboardComponent addDashboardComponentDetail(DashboardComponentDetail dashboardComponentDetail) { + this.dashboardComponentDetails.add(dashboardComponentDetail); + dashboardComponentDetail.setDashboardComponent(this); + return this; + } + + public DashboardComponent removeDashboardComponentDetail(DashboardComponentDetail dashboardComponentDetail) { + this.dashboardComponentDetails.remove(dashboardComponentDetail); + dashboardComponentDetail.setDashboardComponent(null); + return this; + } + + public List getDashboardComponentOrganizations() { + return this.dashboardComponentOrganizations; + } + + public void setDashboardComponentOrganizations(List dashboardComponentOrganizations) { + if (this.dashboardComponentOrganizations != null) { + this.dashboardComponentOrganizations.forEach((v) -> v.setDashboardComponent(null)); + } + if (dashboardComponentOrganizations != null) { + dashboardComponentOrganizations.forEach((v) -> v.setDashboardComponent(this)); + } + this.dashboardComponentOrganizations = dashboardComponentOrganizations; + } + + public DashboardComponent dashboardComponentOrganizations( + List dashboardComponentOrganizations) { + this.setDashboardComponentOrganizations(dashboardComponentOrganizations); + return this; + } + + public DashboardComponent addDashboardComponentOrganization( + DashboardComponentOrganization dashboardComponentOrganization) { + this.dashboardComponentOrganizations.add(dashboardComponentOrganization); + dashboardComponentOrganization.setDashboardComponent(this); + return this; + } + + public DashboardComponent removeDashboardComponentOrganization( + DashboardComponentOrganization dashboardComponentOrganization) { + this.dashboardComponentOrganizations.remove(dashboardComponentOrganization); + dashboardComponentOrganization.setDashboardComponent(null); + return this; + } + + public Boolean getIsExportable() { + return isExportable; + } + + public DashboardComponent isExportable(Boolean isExportable) { + this.setIsExportable(isExportable); + return this; + } + + public void setIsExportable(Boolean isExportable) { + this.isExportable = isExportable; + } + + //@formatter:off + @Override + public String toString() { + return "DashboardComponent{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", type='" + getType() + "'" + + ", view='" + getView() + "'" + + ", chartType='" + getChartType() + "'" + + ", isExportable='" + getIsExportable() + "'" + + ", isActive='" + getIsActive() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentDetail.java b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentDetail.java new file mode 100644 index 0000000..5f3c630 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentDetail.java @@ -0,0 +1,164 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * DashboardComponent + * Defines a component for dashboard. Nothing more than a group of outputs (variables of output type) + */ +@Entity +@Table(name = "dashboard_component_detail") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class DashboardComponentDetail extends AbstractResilientLongIdEntity { + private static final long serialVersionUID = 1L; + + @NotNull + @Column(name = "name", nullable = false, unique = true) + private String name; + + /** + * The color to use for this detail (usually in charts) + */ + @Column(name = "color") + private String color; + + /** + * Sets the order of the collection. When present, all collection queries should make an ORDER BY this column + */ + @NotNull + @Column(name = "idx_order", nullable = false) + private Integer indexOrder; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableUnits", "baseUnit", "variableScope", + "variableCategory" }, allowSetters = true) + private Variable variable; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "dashboardComponentDetails", "dashboardComponentOrganizations" }, allowSetters = true) + private DashboardComponent dashboardComponent; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "dashboardComponentDetails", "dashboardComponentOrganizations" }, allowSetters = true) + private DashboardComponent targetDashboardComponent; + + @Column(name = "help", nullable = true) + private String help; + + public String getName() { + return this.name; + } + + public DashboardComponentDetail name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getHelp() { + return this.help; + } + + public DashboardComponentDetail help(String help) { + this.setHelp(help); + return this; + } + + public void setHelp(String help) { + this.help = help; + } + + public String getColor() { + return this.color; + } + + public DashboardComponentDetail color(String color) { + this.setColor(color); + return this; + } + + public void setColor(String color) { + this.color = color; + } + + public Integer getIndexOrder() { + return this.indexOrder; + } + + public DashboardComponentDetail indexOrder(Integer indexOrder) { + this.setIndexOrder(indexOrder); + return this; + } + + public void setIndexOrder(Integer indexOrder) { + this.indexOrder = indexOrder; + } + + public DashboardComponent getDashboardComponent() { + return this.dashboardComponent; + } + + public void setDashboardComponent(DashboardComponent dashboardComponent) { + this.dashboardComponent = dashboardComponent; + } + + public DashboardComponentDetail dashboardComponent(DashboardComponent dashboardComponent) { + this.setDashboardComponent(dashboardComponent); + return this; + } + + public DashboardComponent getTargetDashboardComponent() { + return this.targetDashboardComponent; + } + + public void setTargetDashboardComponent(DashboardComponent targetDashboardComponent) { + this.targetDashboardComponent = targetDashboardComponent; + } + + public DashboardComponentDetail targetDashboardComponent(DashboardComponent targetDashboardComponent) { + this.setTargetDashboardComponent(targetDashboardComponent); + return this; + } + + public Variable getVariable() { + return this.variable; + } + + public void setVariable(Variable variable) { + this.variable = variable; + } + + public DashboardComponentDetail variable(Variable variable) { + this.setVariable(variable); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "DashboardComponentDetail{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", help='" + getHelp() + "'" + + ", color='" + getColor() + "'" + + ", indexOrder='" + getIndexOrder() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentDetailValue.java b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentDetailValue.java new file mode 100644 index 0000000..3ad730c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentDetailValue.java @@ -0,0 +1,74 @@ +package com.oguerreiro.resilient.domain; + +import java.math.BigDecimal; + +/** + * DashboardComponentDetailValue + */ +public class DashboardComponentDetailValue { + + private DashboardComponentDetail dashboardComponentDetail; + private DashboardComponent targetDashboardComponent; + private Organization owner; + private BigDecimal value; + + public DashboardComponentDetailValue(DashboardComponentDetail dashboardComponentDetail, + DashboardComponent targetDashboardComponent, Organization owner, BigDecimal value) { + super(); + this.dashboardComponentDetail = dashboardComponentDetail; + this.targetDashboardComponent = targetDashboardComponent; + this.owner = owner; + this.value = value; + } + + public DashboardComponentDetailValue(DashboardComponentDetail dashboardComponentDetail, String owner, + BigDecimal value) { + super(); + this.dashboardComponentDetail = dashboardComponentDetail; + //this.owner = owner; + this.value = value; + } + + public DashboardComponentDetail getDashboardComponentDetail() { + return dashboardComponentDetail; + } + + public void setDashboardComponentDetail(DashboardComponentDetail dashboardComponentDetail) { + this.dashboardComponentDetail = dashboardComponentDetail; + } + + public Organization getOwner() { + return owner; + } + + public void setOwner(Organization owner) { + this.owner = owner; + } + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public DashboardComponent getTargetDashboardComponent() { + return targetDashboardComponent; + } + + public void setTargetDashboardComponent(DashboardComponent targetDashboardComponent) { + this.targetDashboardComponent = targetDashboardComponent; + } + + //@formatter:off + @Override + public String toString() { + return "DashboardComponentDetail{" + + "dashboardComponentDetail.id=" + getDashboardComponentDetail().getId() + + ", owner.code='" + getOwner().getCode() + "'" + + ", value=" + getValue() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentOrganization.java b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentOrganization.java new file mode 100644 index 0000000..09c3004 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/DashboardComponentOrganization.java @@ -0,0 +1,69 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +/** + * DashboardComponentOrganization + * Defines a component restrition, to only some organizations. DashboardComponentDetail's are calculated the same way, + * but filtered to only the allowed Organizations. + */ +@Entity +@Table(name = "dashboard_component_org") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class DashboardComponentOrganization extends AbstractResilientLongIdEntity { + private static final long serialVersionUID = 1L; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "children", "parent", "organizationtype" }, allowSetters = true) + private Organization organization; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "dashboardComponentDetails", "dashboardComponentOrganizations" }, allowSetters = true) + private DashboardComponent dashboardComponent; + + public DashboardComponent getDashboardComponent() { + return this.dashboardComponent; + } + + public void setDashboardComponent(DashboardComponent dashboardComponent) { + this.dashboardComponent = dashboardComponent; + } + + public DashboardComponentOrganization dashboardComponent(DashboardComponent dashboardComponent) { + this.setDashboardComponent(dashboardComponent); + return this; + } + + public Organization getOrganization() { + return this.organization; + } + + public void setOrganization(Organization organization) { + this.organization = organization; + } + + public DashboardComponentOrganization organization(Organization organization) { + this.setOrganization(organization); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "DashboardComponentOrganization {" + + "id=" + getId() + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/Document.java b/src/main/java/com/oguerreiro/resilient/domain/Document.java new file mode 100644 index 0000000..c0d9339 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/Document.java @@ -0,0 +1,110 @@ +package com.oguerreiro.resilient.domain; + +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Document with an attach + */ +@Entity +@Table(name = "document") +@ResilientSecurityResourceConfig() +public class Document extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * A simple and small text with a title, for human reference and better relation + */ + @NotNull + @Column(name = "title", nullable = false) + private String title; + + /** + * An additional optional description + */ + @Column(name = "description", nullable = true) + private String description; + + /** + * An Excel file, that must match a template for input + */ + @Lob + @Column(name = "data_file", nullable = false) + private byte[] dataFile; + + @NotNull + @Column(name = "data_file_content_type", nullable = false) + private String dataFileContentType; + + public String getTitle() { + return this.title; + } + + public Document title(String title) { + this.setTitle(title); + return this; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return this.description; + } + + public Document description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public byte[] getDataFile() { + return this.dataFile; + } + + public Document dataFile(byte[] dataFile) { + this.setDataFile(dataFile); + return this; + } + + public void setDataFile(byte[] dataFile) { + this.dataFile = dataFile; + } + + public String getDataFileContentType() { + return this.dataFileContentType; + } + + public Document dataFileContentType(String dataFileContentType) { + this.dataFileContentType = dataFileContentType; + return this; + } + + public void setDataFileContentType(String dataFileContentType) { + this.dataFileContentType = dataFileContentType; + } + +//@formatter:off + @Override + public String toString() { + return "Document{" + + "id=" + getId() + + ", title='" + getTitle() + "'" + + ", description='" + getDescription() + "'" + + ", dataFile='" + getDataFile() + "'" + + ", dataFileContentType='" + getDataFileContentType() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/EmissionFactor.java b/src/main/java/com/oguerreiro/resilient/domain/EmissionFactor.java new file mode 100644 index 0000000..7747253 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/EmissionFactor.java @@ -0,0 +1,217 @@ +package com.oguerreiro.resilient.domain; + +import java.math.BigDecimal; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * Emission Factor + */ +@Entity +@Table(name = "emission_factor") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class EmissionFactor extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + @NotNull + @Column(name = "year", nullable = false) + private Integer year; + + /** + * Unique key, within the year + */ + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Column(name = "code", nullable = false) + private String code; + + /** + * The name or title for the Emission factor + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + /** + * The unit of the Emission Factor. + */ + @NotNull + @Column(name = "unit", nullable = false) + private String unit; + + /** + * The value of the factor. + */ + @NotNull + @Column(name = "value", precision = 21, scale = 6, nullable = false) + private BigDecimal value; + + /** + * The source of the Emission Factor. + */ + @Column(name = "source") + private String source; + + /** + * Comments + */ + @Column(name = "comments") + private String comments; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableCategories", "variables", "emissionFactors" }, allowSetters = true) + private VariableScope variableScope; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variables", "emissionFactors", "variableScope" }, allowSetters = true) + private VariableCategory variableCategory; + + // jhipster-needle-entity-add-field - JHipster will add fields here + + public Integer getYear() { + return this.year; + } + + public EmissionFactor year(Integer year) { + this.setYear(year); + return this; + } + + public void setYear(Integer year) { + this.year = year; + } + + public String getCode() { + return this.code; + } + + public EmissionFactor code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public EmissionFactor name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getUnit() { + return this.unit; + } + + public EmissionFactor unit(String unit) { + this.setUnit(unit); + return this; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public BigDecimal getValue() { + return this.value; + } + + public EmissionFactor value(BigDecimal value) { + this.setValue(value); + return this; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public String getSource() { + return this.source; + } + + public EmissionFactor source(String source) { + this.setSource(source); + return this; + } + + public void setSource(String source) { + this.source = source; + } + + public String getComments() { + return this.comments; + } + + public EmissionFactor comments(String comments) { + this.setComments(comments); + return this; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public VariableScope getVariableScope() { + return this.variableScope; + } + + public void setVariableScope(VariableScope variableScope) { + this.variableScope = variableScope; + } + + public EmissionFactor variableScope(VariableScope variableScope) { + this.setVariableScope(variableScope); + return this; + } + + public VariableCategory getVariableCategory() { + return this.variableCategory; + } + + public void setVariableCategory(VariableCategory variableCategory) { + this.variableCategory = variableCategory; + } + + public EmissionFactor variableCategory(VariableCategory variableCategory) { + this.setVariableCategory(variableCategory); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "EmissionFactor{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", unit='" + getUnit() + "'" + + ", value=" + getValue() + + ", source='" + getSource() + "'" + + ", comments='" + getComments() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/InputData.java b/src/main/java/com/oguerreiro/resilient/domain/InputData.java new file mode 100644 index 0000000..11e96ee --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/InputData.java @@ -0,0 +1,514 @@ +package com.oguerreiro.resilient.domain; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.enumeration.DataSourceType; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Input data Values The values inserted from: - Each Organic Unit - Variable - + * PeriodVersion + */ +@Entity +@Table(name = "input_data") +@ResilientSecurityResourceConfig() +public class InputData extends AbstractResilientLongIdEntity { + private static final long serialVersionUID = 1L; + + /** + * The value of the data when its a string. + */ + @Column(name = "string_value") + private String stringValue; + + /** + * The value of the data in the source Unit. This might be different from the + * Variable baseUnit. @see variableValue and variableUnit + */ + @Column(name = "source_value", precision = 21, scale = 2, nullable = false) + private BigDecimal sourceValue; + + /** + * The value of the data converted to the variable baseUnit. + */ + @Column(name = "variable_value", precision = 21, scale = 2, nullable = false) + private BigDecimal variableValue; + + /** + * The value that the InputData.owner is accounted for. In case a variableValue + * is distributed to other Organization's + * (InputData.source==DataSources.DISTRIB) , this will have the value to + * consider when calculating the final inventory value for the variable + */ + @Column(name = "imputed_value", precision = 21, scale = 2, nullable = false) + private BigDecimal imputedValue; + + /** + * What was the source of this data ? + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "source_type", nullable = false) + private DataSourceType sourceType; + + /** + * The date that this data relates to. Eg. energy consumptium happens every + * month, this should be the date of invoice [optional] + */ + @Column(name = "data_date") + private LocalDate dataDate; + + /** + * Last change date + */ + @NotNull + @Column(name = "change_date", nullable = false) + private Instant changeDate; + + /** + * The actual username logged into the system, that last changed the data + */ + @NotNull + @Column(name = "change_username", nullable = false) + private String changeUsername; + + /** + * Where this data originates from? Where were the data collected ? + */ + @Column(name = "data_source") + private String dataSource; + + /** + * Who collected the data ? + */ + @Column(name = "data_user") + private String dataUser; + + /** + * Comments added by the user + */ + @Column(name = "data_comments") + private String dataComments; + + /** + * The creation date (insert). When the data was inserted into the system. + */ + @NotNull + @Column(name = "creation_date", nullable = false) + private Instant creationDate; + + /** + * The actual username logged into the system, that inserted the data. NOT a + * reference to the user, but the login username. + */ + @NotNull + @Column(name = "creation_username", nullable = false) + private String creationUsername; + + /** + * The variable class code, if selected. Persists the value and NOT a reference + */ + @Column(name = "variable_class_code") + private String variableClassCode; + + /** + * The variable class name, if selected. Persists the value and NOT a reference + */ + @Column(name = "variable_class_name") + private String variableClassName; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "sourceInputData") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "inputData", "variable", "period", "sourceUnit", "unit", "owner", "sourceInputData", + "sourceInputDataUpload", }, allowSetters = true) + private Set inputData = new HashSet<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableClasses", "inputData", "outputData", "variableUnits", "baseUnit", + "variableScope", "variableCategory" }, allowSetters = true) + private Variable variable; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "periodVersions" }, allowSetters = true) + private Period period; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "unitType" }, allowSetters = true) + private Unit sourceUnit; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "unitType" }, allowSetters = true) + private Unit unit; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "user", "children", "parent", "organizationType" }, allowSetters = true) + private Organization owner; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "inputData", "variable", "period", "periodVersion", "sourceUnit", "unit", "owner", + "sourceInputData", "sourceInputDataUpload", }, allowSetters = true) + private InputData sourceInputData; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "inputData", "inputDataUploadLogs", "period", "periodVersion", + "owner" }, allowSetters = true) + private InputDataUpload sourceInputDataUpload; + + public String getStringValue() { + return this.stringValue; + } + + public InputData stringValue(String stringValue) { + this.setStringValue(stringValue); + return this; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public BigDecimal getSourceValue() { + return this.sourceValue; + } + + public InputData sourceValue(BigDecimal sourceValue) { + this.setSourceValue(sourceValue); + return this; + } + + public void setSourceValue(BigDecimal sourceValue) { + this.sourceValue = sourceValue; + } + + public BigDecimal getVariableValue() { + return this.variableValue; + } + + public InputData variableValue(BigDecimal variableValue) { + this.setVariableValue(variableValue); + return this; + } + + public void setVariableValue(BigDecimal variableValue) { + this.variableValue = variableValue; + } + + public BigDecimal getImputedValue() { + return this.imputedValue; + } + + public InputData imputedValue(BigDecimal imputedValue) { + this.setImputedValue(imputedValue); + return this; + } + + public void setImputedValue(BigDecimal imputedValue) { + this.imputedValue = imputedValue; + } + + public DataSourceType getSourceType() { + return this.sourceType; + } + + public InputData sourceType(DataSourceType sourceType) { + this.setSourceType(sourceType); + return this; + } + + public void setSourceType(DataSourceType sourceType) { + this.sourceType = sourceType; + } + + public LocalDate getDataDate() { + return this.dataDate; + } + + public InputData dataDate(LocalDate dataDate) { + this.setDataDate(dataDate); + return this; + } + + public void setDataDate(LocalDate dataDate) { + this.dataDate = dataDate; + } + + public Instant getChangeDate() { + return this.changeDate; + } + + public InputData changeDate(Instant changeDate) { + this.setChangeDate(changeDate); + return this; + } + + public void setChangeDate(Instant changeDate) { + this.changeDate = changeDate; + } + + public String getChangeUsername() { + return this.changeUsername; + } + + public InputData changeUsername(String changeUsername) { + this.setChangeUsername(changeUsername); + return this; + } + + public void setChangeUsername(String changeUsername) { + this.changeUsername = changeUsername; + } + + public String getDataSource() { + return this.dataSource; + } + + public InputData dataSource(String dataSource) { + this.setDataSource(dataSource); + return this; + } + + public void setDataSource(String dataSource) { + this.dataSource = dataSource; + } + + public String getDataUser() { + return this.dataUser; + } + + public InputData dataUser(String dataUser) { + this.setDataUser(dataUser); + return this; + } + + public void setDataUser(String dataUser) { + this.dataUser = dataUser; + } + + public String getDataComments() { + return this.dataComments; + } + + public InputData dataComments(String dataComments) { + this.setDataComments(dataComments); + return this; + } + + public void setDataComments(String dataComments) { + this.dataComments = dataComments; + } + + public Instant getCreationDate() { + return this.creationDate; + } + + public InputData creationDate(Instant creationDate) { + this.setCreationDate(creationDate); + return this; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return this.creationUsername; + } + + public InputData creationUsername(String creationUsername) { + this.setCreationUsername(creationUsername); + return this; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public String getVariableClassCode() { + return this.variableClassCode; + } + + public InputData variableClassCode(String variableClassCode) { + this.setVariableClassCode(variableClassCode); + return this; + } + + public void setVariableClassCode(String variableClassCode) { + this.variableClassCode = variableClassCode; + } + + public String getVariableClassName() { + return this.variableClassName; + } + + public InputData variableClassName(String variableClassName) { + this.setVariableClassName(variableClassName); + return this; + } + + public void setVariableClassName(String variableClassName) { + this.variableClassName = variableClassName; + } + + public Set getInputData() { + return this.inputData; + } + + public void setInputData(Set inputData) { + if (this.inputData != null) { + this.inputData.forEach(i -> i.setSourceInputData(null)); + } + if (inputData != null) { + inputData.forEach(i -> i.setSourceInputData(this)); + } + this.inputData = inputData; + } + + public InputData inputData(Set inputData) { + this.setInputData(inputData); + return this; + } + + public InputData addInputData(InputData inputData) { + this.inputData.add(inputData); + inputData.setSourceInputData(this); + return this; + } + + public InputData removeInputData(InputData inputData) { + this.inputData.remove(inputData); + inputData.setSourceInputData(null); + return this; + } + + public Variable getVariable() { + return this.variable; + } + + public void setVariable(Variable variable) { + this.variable = variable; + } + + public InputData variable(Variable variable) { + this.setVariable(variable); + return this; + } + + public Period getPeriod() { + return this.period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public InputData period(Period period) { + this.setPeriod(period); + return this; + } + + public Unit getSourceUnit() { + return this.sourceUnit; + } + + public void setSourceUnit(Unit unit) { + this.sourceUnit = unit; + } + + public InputData sourceUnit(Unit unit) { + this.setSourceUnit(unit); + return this; + } + + public Unit getUnit() { + return this.unit; + } + + public void setUnit(Unit unit) { + this.unit = unit; + } + + public InputData unit(Unit unit) { + this.setUnit(unit); + return this; + } + + public Organization getOwner() { + return this.owner; + } + + public void setOwner(Organization organization) { + this.owner = organization; + } + + public InputData owner(Organization organization) { + this.setOwner(organization); + return this; + } + + public InputData getSourceInputData() { + return this.sourceInputData; + } + + public void setSourceInputData(InputData inputData) { + this.sourceInputData = inputData; + } + + public InputData sourceInputData(InputData inputData) { + this.setSourceInputData(inputData); + return this; + } + + public InputDataUpload getSourceInputDataUpload() { + return this.sourceInputDataUpload; + } + + public void setSourceInputDataUpload(InputDataUpload inputDataUpload) { + this.sourceInputDataUpload = inputDataUpload; + } + + public InputData sourceInputDataUpload(InputDataUpload inputDataUpload) { + this.setSourceInputDataUpload(inputDataUpload); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "InputData{" + + "id=" + getId() + + ", sourceValue=" + getSourceValue() + + ", variableValue=" + getVariableValue() + + ", imputedValue=" + getImputedValue() + + ", stringValue='" + getStringValue() + "'" + + ", sourceType='" + getSourceType() + "'" + + ", dataDate='" + getDataDate() + "'" + + ", changeDate='" + getChangeDate() + "'" + + ", changeUsername='" + getChangeUsername() + "'" + + ", dataSource='" + getDataSource() + "'" + + ", dataUser='" + getDataUser() + "'" + + ", dataComments='" + getDataComments() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/InputDataUpload.java b/src/main/java/com/oguerreiro/resilient/domain/InputDataUpload.java new file mode 100644 index 0000000..76cee15 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/InputDataUpload.java @@ -0,0 +1,362 @@ +package com.oguerreiro.resilient.domain; + +import java.io.Serializable; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.activity.AbstractActivityDomain; +import com.oguerreiro.resilient.domain.enumeration.UploadStatus; +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Input data upload file Users can upload an Excel file with InputData that + * will automatically insert values in the table InputData + */ +@Entity +@Table(name = "input_data_upload") +@ResilientSecurityResourceConfig() +public class InputDataUpload extends AbstractActivityDomain implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * A simple text with a title, for human reference and better relation + */ + @NotNull + @Column(name = "title", nullable = false) + private String title; + + /** + * An Excel file, that must match a template for input + */ + @Lob + @Column(name = "data_file", nullable = false) + private byte[] dataFile; + + @NotNull + @Column(name = "data_file_content_type", nullable = false) + private String dataFileContentType; + + /** + * The name of the uploaded Excel file + */ + @Column(name = "upload_file_name") + private String uploadFileName; + + /** + * The generated name of the Excel file, used to save to disk + */ + @Column(name = "disk_file_name") + private String diskFileName; + + /** + * Type of the Upload + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + private UploadType type; + + /** + * State of the Upload + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "state", nullable = false) + private UploadStatus state; + + /** + * Optional comments + */ + @Column(name = "comments") + private String comments; + + /** + * The creation date (insert). When the data was inserted into the system. + */ + @NotNull + @Column(name = "creation_date", nullable = false) + private Instant creationDate; + + /** + * The actual username logged into the system, that inserted the data. NOT a + * reference to the user, but the login username. + */ + @NotNull + @Column(name = "creation_username", nullable = false) + private String creationUsername; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "sourceInputDataUpload") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "inputData", "variable", "period", "sourceUnit", "unit", "owner", "sourceInputData", + "sourceInputDataUpload", }, allowSetters = true) + private Set inputData = new HashSet<>(); + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "inputDataUpload") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "inputDataUpload" }, allowSetters = true) + private Set inputDataUploadLogs = new HashSet<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "periodVersions" }, allowSetters = true) + private Period period; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "user", "children", "parent", "organizationType" }, allowSetters = true) + private Organization owner; + + public String getTitle() { + return this.title; + } + + public InputDataUpload title(String title) { + this.setTitle(title); + return this; + } + + public void setTitle(String title) { + this.title = title; + } + + public byte[] getDataFile() { + return this.dataFile; + } + + public InputDataUpload dataFile(byte[] dataFile) { + this.setDataFile(dataFile); + return this; + } + + public void setDataFile(byte[] dataFile) { + this.dataFile = dataFile; + } + + public String getDataFileContentType() { + return this.dataFileContentType; + } + + public InputDataUpload dataFileContentType(String dataFileContentType) { + this.dataFileContentType = dataFileContentType; + return this; + } + + public void setDataFileContentType(String dataFileContentType) { + this.dataFileContentType = dataFileContentType; + } + + public String getUploadFileName() { + return this.uploadFileName; + } + + public InputDataUpload uploadFileName(String uploadFileName) { + this.setUploadFileName(uploadFileName); + return this; + } + + public void setUploadFileName(String uploadFileName) { + this.uploadFileName = uploadFileName; + } + + public String getDiskFileName() { + return this.diskFileName; + } + + public InputDataUpload diskFileName(String diskFileName) { + this.setDiskFileName(diskFileName); + return this; + } + + public void setDiskFileName(String diskFileName) { + this.diskFileName = diskFileName; + } + + public UploadType getType() { + return this.type; + } + + public InputDataUpload type(UploadType type) { + this.setType(type); + return this; + } + + public void setType(UploadType type) { + this.type = type; + } + + public UploadStatus getState() { + return this.state; + } + + public InputDataUpload state(UploadStatus state) { + this.setState(state); + return this; + } + + public void setState(UploadStatus state) { + this.state = state; + } + + public String getComments() { + return this.comments; + } + + public InputDataUpload comments(String comments) { + this.setComments(comments); + return this; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public Instant getCreationDate() { + return this.creationDate; + } + + public InputDataUpload creationDate(Instant creationDate) { + this.setCreationDate(creationDate); + return this; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return this.creationUsername; + } + + public InputDataUpload creationUsername(String creationUsername) { + this.setCreationUsername(creationUsername); + return this; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public Set getInputData() { + return this.inputData; + } + + public void setInputData(Set inputData) { + if (this.inputData != null) { + this.inputData.forEach(i -> i.setSourceInputDataUpload(null)); + } + if (inputData != null) { + inputData.forEach(i -> i.setSourceInputDataUpload(this)); + } + this.inputData = inputData; + } + + public InputDataUpload inputData(Set inputData) { + this.setInputData(inputData); + return this; + } + + public InputDataUpload addInputData(InputData inputData) { + this.inputData.add(inputData); + inputData.setSourceInputDataUpload(this); + return this; + } + + public InputDataUpload removeInputData(InputData inputData) { + this.inputData.remove(inputData); + inputData.setSourceInputDataUpload(null); + return this; + } + + public Set getInputDataUploadLogs() { + return this.inputDataUploadLogs; + } + + public void setInputDataUploadLogs(Set inputDataUploadLogs) { + if (this.inputDataUploadLogs != null) { + this.inputDataUploadLogs.forEach(i -> i.setInputDataUpload(null)); + } + if (inputDataUploadLogs != null) { + inputDataUploadLogs.forEach(i -> i.setInputDataUpload(this)); + } + this.inputDataUploadLogs = inputDataUploadLogs; + } + + public InputDataUpload inputDataUploadLogs(Set inputDataUploadLogs) { + this.setInputDataUploadLogs(inputDataUploadLogs); + return this; + } + + public InputDataUpload addInputDataUploadLog(InputDataUploadLog inputDataUploadLog) { + this.inputDataUploadLogs.add(inputDataUploadLog); + inputDataUploadLog.setInputDataUpload(this); + return this; + } + + public InputDataUpload removeInputDataUploadLog(InputDataUploadLog inputDataUploadLog) { + this.inputDataUploadLogs.remove(inputDataUploadLog); + inputDataUploadLog.setInputDataUpload(null); + return this; + } + + public Period getPeriod() { + return this.period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public InputDataUpload period(Period period) { + this.setPeriod(period); + return this; + } + + public Organization getOwner() { + return this.owner; + } + + public void setOwner(Organization organization) { + this.owner = organization; + } + + public InputDataUpload owner(Organization organization) { + this.setOwner(organization); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "InputDataUpload{" + + "id=" + getId() + + ", title='" + getTitle() + "'" + + ", dataFile='" + getDataFile() + "'" + + ", dataFileContentType='" + getDataFileContentType() + "'" + + ", uploadFileName='" + getUploadFileName() + "'" + + ", diskFileName='" + getDiskFileName() + "'" + + ", type='" + getType() + "'" + + ", state='" + getState() + "'" + + ", comments='" + getComments() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/InputDataUploadLog.java b/src/main/java/com/oguerreiro/resilient/domain/InputDataUploadLog.java new file mode 100644 index 0000000..77c32d7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/InputDataUploadLog.java @@ -0,0 +1,118 @@ +package com.oguerreiro.resilient.domain; + +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Input data upload file + * Users can upload an Excel file with InputData that will automatically insert values in the table InputData + */ +@Entity +@Table(name = "input_data_upload_log") +@ResilientSecurityResourceConfig() +public class InputDataUploadLog extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * The log of the event. Eg. \"InputDataUpload created\"; \"InputDataUpload replaced with new file\"; \"125 records + * inserted\"; \"User XPTO confirmed the file replacement.\" + */ + @NotNull + @Column(name = "log_message", nullable = false) + private String logMessage; + + /** + * The creation date (insert). When the data was inserted into the system. + */ + @NotNull + @Column(name = "creation_date", nullable = false) + private Instant creationDate; + + /** + * The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login + * username. + */ + @NotNull + @Column(name = "creation_username", nullable = false) + private String creationUsername; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "inputData", "inputDataUploadLogs", "period", "periodVersion", + "owner" }, allowSetters = true) + private InputDataUpload inputDataUpload; + + public String getLogMessage() { + return this.logMessage; + } + + public InputDataUploadLog logMessage(String logMessage) { + this.setLogMessage(logMessage); + return this; + } + + public void setLogMessage(String logMessage) { + this.logMessage = logMessage; + } + + public Instant getCreationDate() { + return this.creationDate; + } + + public InputDataUploadLog creationDate(Instant creationDate) { + this.setCreationDate(creationDate); + return this; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return this.creationUsername; + } + + public InputDataUploadLog creationUsername(String creationUsername) { + this.setCreationUsername(creationUsername); + return this; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public InputDataUpload getInputDataUpload() { + return this.inputDataUpload; + } + + public void setInputDataUpload(InputDataUpload inputDataUpload) { + this.inputDataUpload = inputDataUpload; + } + + public InputDataUploadLog inputDataUpload(InputDataUpload inputDataUpload) { + this.setInputDataUpload(inputDataUpload); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "InputDataUploadLog{" + + "id=" + getId() + + ", logMessage='" + getLogMessage() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/InventoryData.java b/src/main/java/com/oguerreiro/resilient/domain/InventoryData.java new file mode 100644 index 0000000..9fa1613 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/InventoryData.java @@ -0,0 +1,80 @@ +package com.oguerreiro.resilient.domain; + +import java.math.BigDecimal; + +public class InventoryData { + private Variable variable; + private String variableClassCode; + private String variableClassName; + private Unit unit; + private BigDecimal value; + private String stringValue; + + public InventoryData(Variable variable, String variableClassCode, String variableClassName, Unit unit, + String stringValue, BigDecimal value) { + super(); + this.variable = variable; + this.variableClassCode = variableClassCode; + this.variableClassName = variableClassName; + this.unit = unit; + this.value = value; + this.stringValue = stringValue; + } + + public Variable getVariable() { + return variable; + } + + public void setVariable(Variable variable) { + this.variable = variable; + } + + public String getVariableClassCode() { + return variableClassCode; + } + + public void setVariableClassCode(String variableClassCode) { + this.variableClassCode = variableClassCode; + } + + public String getVariableClassName() { + return variableClassName; + } + + public void setVariableClassName(String variableClassName) { + this.variableClassName = variableClassName; + } + + public Unit getUnit() { + return unit; + } + + public void setUnit(Unit unit) { + this.unit = unit; + } + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + //@formatter:off + @Override + public String toString() { + return "InventoryData{" + + ", value=" + getValue() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/MetadataProperty.java b/src/main/java/com/oguerreiro/resilient/domain/MetadataProperty.java new file mode 100644 index 0000000..99955c1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/MetadataProperty.java @@ -0,0 +1,189 @@ +package com.oguerreiro.resilient.domain; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.enumeration.MetadataType; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * Metadata Property + * Configuration of a metadata property. Defines its type and a description of its usage. + */ +@Entity +@Table(name = "metadata_property") +@ResilientSecurityResourceConfig() +public class MetadataProperty extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + @NotNull + @Size(min = 3) + @Column(name = "code", nullable = false, unique = true) + private String code; + + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description") + private String description; + + @Column(name = "mandatory") + private Boolean mandatory; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "metadata_type", nullable = false) + private MetadataType metadataType; + + /** + * Regex pattern for validation (optional). Only applies when the value is not empty. + */ + @Column(name = "pattern") + private String pattern; + + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "metadataProperties") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "organizations", "metadataProperties" }, allowSetters = true) + private Set organizationTypes = new HashSet<>(); + + public String getCode() { + return this.code; + } + + public MetadataProperty code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public MetadataProperty name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public MetadataProperty description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getMandatory() { + return this.mandatory; + } + + public MetadataProperty mandatory(Boolean mandatory) { + this.setMandatory(mandatory); + return this; + } + + public void setMandatory(Boolean mandatory) { + this.mandatory = mandatory; + } + + public MetadataType getMetadataType() { + return this.metadataType; + } + + public MetadataProperty metadataType(MetadataType metadataType) { + this.setMetadataType(metadataType); + return this; + } + + public void setMetadataType(MetadataType metadataType) { + this.metadataType = metadataType; + } + + public String getPattern() { + return this.pattern; + } + + public MetadataProperty pattern(String pattern) { + this.setPattern(pattern); + return this; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Set getOrganizationTypes() { + return this.organizationTypes; + } + + public void setOrganizationTypes(Set organizationTypes) { + if (this.organizationTypes != null) { + this.organizationTypes.forEach(i -> i.removeMetadataProperties(this)); + } + if (organizationTypes != null) { + organizationTypes.forEach(i -> i.addMetadataProperties(this)); + } + this.organizationTypes = organizationTypes; + } + + public MetadataProperty organizationTypes(Set organizationTypes) { + this.setOrganizationTypes(organizationTypes); + return this; + } + + public MetadataProperty addOrganizationType(OrganizationType organizationType) { + this.organizationTypes.add(organizationType); + organizationType.getMetadataProperties().add(this); + return this; + } + + public MetadataProperty removeOrganizationType(OrganizationType organizationType) { + this.organizationTypes.remove(organizationType); + organizationType.getMetadataProperties().remove(this); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "MetadataProperty{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", mandatory='" + getMandatory() + "'" + + ", metadataType='" + getMetadataType() + "'" + + ", pattern='" + getPattern() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/MetadataValue.java b/src/main/java/com/oguerreiro/resilient/domain/MetadataValue.java new file mode 100644 index 0000000..905117c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/MetadataValue.java @@ -0,0 +1,119 @@ +package com.oguerreiro.resilient.domain; + +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Metadata Value + * The value of a metadata property. + */ +@Entity +@Table(name = "metadata_value") +@ResilientSecurityResourceConfig() +public class MetadataValue extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * Functional relational key to MetadataProperty + */ + @NotNull + @Column(name = "metadata_property_code", nullable = false) + private String metadataPropertyCode; + + /** + * The target domain key that this MetadataValue belongs. Its a class name or a key provided by the consumer.
+ * Used with targetDomainId to create as unique relation with the target domain + * + * @see targetDomainId + */ + @NotNull + @Column(name = "target_domain_key", nullable = false) + private String targetDomainKey; + + /** + * The target domain Id that this MetadataValue belongs.
+ * Used with targetDomainKey to create as unique relation with the target domain + * + * @see targetDomainKey + */ + @NotNull + @Column(name = "target_domain_id", nullable = false) + private Long targetDomainId; + + /** + * The value of the MetadataProperty, persisted as a string.
+ */ + @Column(name = "value") + private String value; + + public String getMetadataPropertyCode() { + return this.metadataPropertyCode; + } + + public MetadataValue metadataPropertyCode(String metadataPropertyCode) { + this.setMetadataPropertyCode(metadataPropertyCode); + return this; + } + + public void setMetadataPropertyCode(String metadataPropertyCode) { + this.metadataPropertyCode = metadataPropertyCode; + } + + public String getTargetDomainKey() { + return this.targetDomainKey; + } + + public MetadataValue targetDomainKey(String targetDomainKey) { + this.setTargetDomainKey(targetDomainKey); + return this; + } + + public void setTargetDomainKey(String targetDomainKey) { + this.targetDomainKey = targetDomainKey; + } + + public Long getTargetDomainId() { + return this.targetDomainId; + } + + public MetadataValue targetDomainId(Long targetDomainId) { + this.setTargetDomainId(targetDomainId); + return this; + } + + public void setTargetDomainId(Long targetDomainId) { + this.targetDomainId = targetDomainId; + } + + public String getValue() { + return this.value; + } + + public MetadataValue value(String value) { + this.setValue(value); + return this; + } + + public void setValue(String value) { + this.value = value; + } + +//@formatter:off + @Override + public String toString() { + return "MetadataValue{" + + "id=" + getId() + + ", metadataPropertyCode='" + getMetadataPropertyCode() + "'" + + ", targetDomainKey='" + getTargetDomainKey() + "'" + + ", targetDomainId=" + getTargetDomainId() + + ", value='" + getValue() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/Organization.java b/src/main/java/com/oguerreiro/resilient/domain/Organization.java new file mode 100644 index 0000000..4728258 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/Organization.java @@ -0,0 +1,292 @@ +package com.oguerreiro.resilient.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Formula; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * Organization + * Hierarchical organigram of the organization. + */ +@Entity +@Table(name = "organization") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class Organization extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * User defined code. Must be unique in the Organization + */ + @NotNull + @Size(min = 3) + @Column(name = "code", nullable = false, unique = true) + private String code; + + /** + * The name of the Organization entry. Allows duplicate name's, but NOT duplicate code's. + */ + @NotNull + @Size(min = 3) + @Column(name = "name", nullable = false) + private String name; + + @Lob + @Column(name = "image") + private byte[] image; + + @Column(name = "image_content_type") + private String imageContentType; + + @Column(name = "input_inventory") + private Boolean inputInventory; + + @Column(name = "output_inventory") + private Boolean outputInventory; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(unique = true) + private User user; + + // Virtual property to use in children @OrderBy statement + @Formula("(CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END)") + private Integer virtualParentOrder; + // Virtual property to use in children @OrderBy statement + @Formula("(CASE WHEN sort IS NULL THEN 1 ELSE 0 END)") + private Integer virtualNullSortOrder; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @OrderBy("virtualParentOrder ASC, virtualNullSortOrder ASC, sort ASC") + @JsonIgnoreProperties(value = { "user", "children", "inputData", "inputDataUploads", "parent", + "organizationType" }, allowSetters = true) + private List children = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "user", "children", "inputData", "inputDataUploads", "parent", + "organizationType" }, allowSetters = true) + private Organization parent; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "organizations", "metadataProperties" }, allowSetters = true) + private OrganizationType organizationType; + + @Column(name = "sort") + private Integer sort; + + public String getCode() { + return this.code; + } + + public Organization code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public Organization name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getSort() { + return this.sort; + } + + public Organization sort(Integer sort) { + this.setSort(sort); + return this; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public byte[] getImage() { + return this.image; + } + + public Organization image(byte[] image) { + this.setImage(image); + return this; + } + + public void setImage(byte[] image) { + this.image = image; + } + + public String getImageContentType() { + return this.imageContentType; + } + + public Organization imageContentType(String imageContentType) { + this.imageContentType = imageContentType; + return this; + } + + public void setImageContentType(String imageContentType) { + this.imageContentType = imageContentType; + } + + public Boolean getInputInventory() { + return this.inputInventory; + } + + public Organization inputInventory(Boolean inputInventory) { + this.setInputInventory(inputInventory); + return this; + } + + public void setInputInventory(Boolean inputInventory) { + this.inputInventory = inputInventory; + } + + public Boolean getOutputInventory() { + return this.outputInventory; + } + + public Organization outputInventory(Boolean outputInventory) { + this.setOutputInventory(outputInventory); + return this; + } + + public void setOutputInventory(Boolean outputInventory) { + this.outputInventory = outputInventory; + } + + public User getUser() { + return this.user; + } + + public void setUser(User user) { + this.user = user; + } + + public Organization user(User user) { + this.setUser(user); + return this; + } + + public List getChildren() { + return this.children; + } + + public void setChildren(List organizations) { + if (this.children != null) { + this.children.forEach(i -> i.setParent(null)); + } + if (organizations != null) { + organizations.forEach(i -> i.setParent(this)); + } + this.children = organizations; + } + + public Organization children(List organizations) { + this.setChildren(organizations); + return this; + } + + public Organization addChild(Organization organization) { + this.children.add(organization); + organization.setParent(this); + return this; + } + + public Organization removeChild(Organization organization) { + this.children.remove(organization); + organization.setParent(null); + return this; + } + + public Organization getParent() { + return this.parent; + } + + public void setParent(Organization organization) { + this.parent = organization; + } + + public Organization parent(Organization organization) { + this.setParent(organization); + return this; + } + + public OrganizationType getOrganizationType() { + return this.organizationType; + } + + public void setOrganizationType(OrganizationType organizationType) { + this.organizationType = organizationType; + } + + public Organization organizationType(OrganizationType organizationType) { + this.setOrganizationType(organizationType); + return this; + } + + public Boolean isChildOf(Organization organization) { + Organization parent = null; + Boolean found = false; + + do { + parent = this.getParent(); + if (parent == null) { + break; + } + if (parent.equals(organization)) { + found = true; + break; + } + } while (true); + + return found; + } + +//@formatter:off + @Override + public String toString() { + return "Organization{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", sort='" + getSort() + "'" + + ", image='" + getImage() + "'" + + ", imageContentType='" + getImageContentType() + "'" + + ", inputInventory='" + getInputInventory() + "'" + + ", outputInventory='" + getOutputInventory() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/OrganizationType.java b/src/main/java/com/oguerreiro/resilient/domain/OrganizationType.java new file mode 100644 index 0000000..5bc7b07 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/OrganizationType.java @@ -0,0 +1,240 @@ +package com.oguerreiro.resilient.domain; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * Organization Type + * The organization type is a setup that defines the usage of the Organization level and other configurations. + */ +@Entity +@Table(name = "organization_type") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class OrganizationType extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * User defined code. Must be unique in the OrganizationType + */ + @NotNull + @Size(min = 1) + @Column(name = "code", nullable = false, unique = true) + private String code; + + /** + * The name of the type. Must be unique in the system + */ + @NotNull + @Size(min = 3) + @Column(name = "name", nullable = false, unique = true) + private String name; + + /** + * A more complete description of the type. Whats the meaning of this type? + */ + @NotNull + @Size(min = 3) + @Column(name = "description", nullable = false) + private String description; + + /** + * The nature of the type + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "nature", nullable = false) + private OrganizationNature nature; + + /** + * Icon image associated with this organization type. Will be used in user interface. + */ + @Lob + @Column(name = "icon") + private byte[] icon; + + @Column(name = "icon_content_type") + private String iconContentType; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "organizationType") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "user", "children", "inputData", "inputDataUploads", "parent", + "organizationType" }, allowSetters = true) + private Set organizations = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "rel_organization_type__metadata_properties", joinColumns = @JoinColumn(name = "organization_type_id"), inverseJoinColumns = @JoinColumn(name = "metadata_properties_id")) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "organizationTypes" }, allowSetters = true) + private Set metadataProperties = new HashSet<>(); + + public String getCode() { + return this.code; + } + + public OrganizationType code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public OrganizationType name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public OrganizationType description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public OrganizationNature getNature() { + return this.nature; + } + + public OrganizationType nature(OrganizationNature nature) { + this.setNature(nature); + return this; + } + + public void setNature(OrganizationNature nature) { + this.nature = nature; + } + + public byte[] getIcon() { + return this.icon; + } + + public OrganizationType icon(byte[] icon) { + this.setIcon(icon); + return this; + } + + public void setIcon(byte[] icon) { + this.icon = icon; + } + + public String getIconContentType() { + return this.iconContentType; + } + + public OrganizationType iconContentType(String iconContentType) { + this.iconContentType = iconContentType; + return this; + } + + public void setIconContentType(String iconContentType) { + this.iconContentType = iconContentType; + } + + public Set getOrganizations() { + return this.organizations; + } + + public void setOrganizations(Set organizations) { + if (this.organizations != null) { + this.organizations.forEach(i -> i.setOrganizationType(null)); + } + if (organizations != null) { + organizations.forEach(i -> i.setOrganizationType(this)); + } + this.organizations = organizations; + } + + public OrganizationType organizations(Set organizations) { + this.setOrganizations(organizations); + return this; + } + + public OrganizationType addOrganization(Organization organization) { + this.organizations.add(organization); + organization.setOrganizationType(this); + return this; + } + + public OrganizationType removeOrganization(Organization organization) { + this.organizations.remove(organization); + organization.setOrganizationType(null); + return this; + } + + public Set getMetadataProperties() { + return this.metadataProperties; + } + + public void setMetadataProperties(Set metadataProperties) { + this.metadataProperties = metadataProperties; + } + + public OrganizationType metadataProperties(Set metadataProperties) { + this.setMetadataProperties(metadataProperties); + return this; + } + + public OrganizationType addMetadataProperties(MetadataProperty metadataProperty) { + this.metadataProperties.add(metadataProperty); + return this; + } + + public OrganizationType removeMetadataProperties(MetadataProperty metadataProperty) { + this.metadataProperties.remove(metadataProperty); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "OrganizationType{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", nature='" + getNature() + "'" + + ", icon='" + getIcon() + "'" + + ", iconContentType='" + getIconContentType() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/OutputData.java b/src/main/java/com/oguerreiro/resilient/domain/OutputData.java new file mode 100644 index 0000000..422e320 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/OutputData.java @@ -0,0 +1,148 @@ +package com.oguerreiro.resilient.domain; + +import java.math.BigDecimal; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Output data Values + * The values calculated using the Variable.outputFormula, for all variables with Variable.output==TRUE. + */ +@Entity +@Table(name = "output_data") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class OutputData extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * The evaluated value, calculated by Variable.outputFormula. This will probably represent an emission value. + */ + @NotNull + @Column(name = "value", precision = 21, scale = 2, nullable = false) + private BigDecimal value; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableClasses", "inputData", "outputData", "variableUnits", "baseUnit", + "variableScope", "variableCategory" }, allowSetters = true) + private Variable variable; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "periodVersions" }, allowSetters = true) + private Period period; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "period" }, allowSetters = true) + private PeriodVersion periodVersion; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "fromUnits", "toUnits", "baseUnits", "sourceUnits", "inputData", "outputData", + "variableUnits", "unitType" }, allowSetters = true) + private Unit baseUnit; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "user", "children", "parent", "organizationType" }, allowSetters = true) + private Organization owner; + + public BigDecimal getValue() { + return this.value; + } + + public OutputData value(BigDecimal value) { + this.setValue(value); + return this; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public Variable getVariable() { + return this.variable; + } + + public void setVariable(Variable variable) { + this.variable = variable; + } + + public OutputData variable(Variable variable) { + this.setVariable(variable); + return this; + } + + public Period getPeriod() { + return this.period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public OutputData period(Period period) { + this.setPeriod(period); + return this; + } + + public PeriodVersion getPeriodVersion() { + return this.periodVersion; + } + + public void setPeriodVersion(PeriodVersion periodVersion) { + this.periodVersion = periodVersion; + } + + public OutputData periodVersion(PeriodVersion periodVersion) { + this.setPeriodVersion(periodVersion); + return this; + } + + public Unit getBaseUnit() { + return this.baseUnit; + } + + public void setBaseUnit(Unit unit) { + this.baseUnit = unit; + } + + public OutputData baseUnit(Unit unit) { + this.setBaseUnit(unit); + return this; + } + + public Organization getOwner() { + return this.owner; + } + + public void setOwner(Organization organization) { + this.owner = organization; + } + + public OutputData owner(Organization organization) { + this.setOwner(organization); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "OutputData{" + + "id=" + getId() + + ", value=" + getValue() + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/Period.java b/src/main/java/com/oguerreiro/resilient/domain/Period.java new file mode 100644 index 0000000..1a79173 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/Period.java @@ -0,0 +1,262 @@ +package com.oguerreiro.resilient.domain; + +import java.io.Serializable; +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.activities.period.close.PeriodCloseActivity; +import com.oguerreiro.resilient.activities.period.process.PeriodProcessOutputsActivity; +import com.oguerreiro.resilient.activity.AbstractActivityDomain; +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Period
+ * Domain that controls inventory data life cycle. An inventory as several + * phases: + *
    + *
  • Gathering data - The first step is to insert the data source for the + * inventory. This can only be done with the {@link Period#getState() + * Period.state} in state {@link PeriodStatus#OPENED}.
    + * Data can be inserted by 2 ways. + *
      + *
    • Manually - directly in the APP using the {@link InputData} domain.
    • + *
    • Import files - using the {@link InputDataUpload} domain.
    • + *
    + *
  • + *
  • Process & Calculate Outputs - Once all inventory data is gathered, output + * must be generated. First the {@link Period} must be + * {@link PeriodStatus#CLOSED} for data input. To do this, use the following + * {@link Activity Activities}: + *
      + *
    1. {@link PeriodCloseActivity} - to close the {@link Period} for data + * input.
    2. + *
    3. {@link PeriodProcessOutputsActivity} - to calculate and generate output + * variables.
    4. + *
        + * + *
+ * For more info on {@link PeriodStatus states} transitions and {@link Activity + * activities}, see {@link PeriodStatus} + */ +@Entity +@Table(name = "period") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class Period extends AbstractActivityDomain implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Name of the period + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + /** + * Description for the period + */ + @Column(name = "description") + private String description; + + /** + * Period start date + */ + @NotNull + @Column(name = "begin_date", nullable = false) + private LocalDate beginDate; + + /** + * Period end date + */ + @NotNull + @Column(name = "end_date", nullable = false) + private LocalDate endDate; + + /** + * State of the Period + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "state", nullable = false) + private PeriodStatus state; + + /** + * The creation date (insert). When the data was inserted into the system. + */ + @NotNull + @Column(name = "creation_date", nullable = false) + private Instant creationDate; + + /** + * The actual username logged into the system, that inserted the data. NOT a + * reference to the user, but the login username. + */ + @NotNull + @Column(name = "creation_username", nullable = false) + private String creationUsername; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "period", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("periodVersion ASC") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "period" }, allowSetters = true) + private List periodVersions = new ArrayList<>(); + + public String getName() { + return this.name; + } + + public Period name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public Period description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public LocalDate getBeginDate() { + return this.beginDate; + } + + public Period beginDate(LocalDate beginDate) { + this.setBeginDate(beginDate); + return this; + } + + public void setBeginDate(LocalDate beginDate) { + this.beginDate = beginDate; + } + + public LocalDate getEndDate() { + return this.endDate; + } + + public Period endDate(LocalDate endDate) { + this.setEndDate(endDate); + return this; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public PeriodStatus getState() { + return this.state; + } + + public Period state(PeriodStatus state) { + this.setState(state); + return this; + } + + public void setState(PeriodStatus state) { + this.state = state; + } + + public Instant getCreationDate() { + return this.creationDate; + } + + public Period creationDate(Instant creationDate) { + this.setCreationDate(creationDate); + return this; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return this.creationUsername; + } + + public Period creationUsername(String creationUsername) { + this.setCreationUsername(creationUsername); + return this; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public List getPeriodVersions() { + return this.periodVersions; + } + + public void setPeriodVersions(List periodVersions) { + if (this.periodVersions != null) { + this.periodVersions.forEach(i -> i.setPeriod(null)); + } + if (periodVersions != null) { + periodVersions.forEach(i -> i.setPeriod(this)); + } + this.periodVersions = periodVersions; + } + + public Period periodVersions(List periodVersions) { + this.setPeriodVersions(periodVersions); + return this; + } + + public Period addPeriodVersion(PeriodVersion periodVersion) { + this.periodVersions.add(periodVersion); + periodVersion.setPeriod(this); + return this; + } + + public Period removePeriodVersion(PeriodVersion periodVersion) { + this.periodVersions.remove(periodVersion); + periodVersion.setPeriod(null); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "Period{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", beginDate='" + getBeginDate() + "'" + + ", endDate='" + getEndDate() + "'" + + ", state='" + getState() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/PeriodVersion.java b/src/main/java/com/oguerreiro/resilient/domain/PeriodVersion.java new file mode 100644 index 0000000..d6b96e9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/PeriodVersion.java @@ -0,0 +1,187 @@ +package com.oguerreiro.resilient.domain; + +import java.time.Instant; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.enumeration.PeriodVersionStatus; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Period versions Always have at least one version. + */ +@Entity +@Table(name = "period_version") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class PeriodVersion extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * Name of the version + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + /** + * Description for the version + */ + @Column(name = "description") + private String description; + + /** + * Period version (sequencial) + */ + @NotNull + @Column(name = "period_version", nullable = false) + private Integer periodVersion; + + /** + * State of the Period + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "state", nullable = false) + private PeriodVersionStatus state; + + /** + * The creation date (insert). When the data was inserted into the system. + */ + @NotNull + @Column(name = "creation_date", nullable = false) + private Instant creationDate; + + /** + * The actual username logged into the system, that inserted the data. NOT a + * reference to the user, but the login username. + */ + @NotNull + @Column(name = "creation_username", nullable = false) + private String creationUsername; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "period_id") + @JsonIgnoreProperties(value = { "periodVersions" }, allowSetters = true) + private Period period; + + public String getName() { + return this.name; + } + + public PeriodVersion name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public PeriodVersion description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getPeriodVersion() { + return this.periodVersion; + } + + public PeriodVersion periodVersion(Integer periodVersion) { + this.setPeriodVersion(periodVersion); + return this; + } + + public void setPeriodVersion(Integer periodVersion) { + this.periodVersion = periodVersion; + } + + public PeriodVersionStatus getState() { + return this.state; + } + + public PeriodVersion state(PeriodVersionStatus state) { + this.setState(state); + return this; + } + + public void setState(PeriodVersionStatus state) { + this.state = state; + } + + public Instant getCreationDate() { + return this.creationDate; + } + + public PeriodVersion creationDate(Instant creationDate) { + this.setCreationDate(creationDate); + return this; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return this.creationUsername; + } + + public PeriodVersion creationUsername(String creationUsername) { + this.setCreationUsername(creationUsername); + return this; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public Period getPeriod() { + return this.period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public PeriodVersion period(Period period) { + this.setPeriod(period); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "PeriodVersion{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", periodVersion=" + getPeriodVersion() + + ", state='" + getState() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/PersistentToken.java b/src/main/java/com/oguerreiro/resilient/domain/PersistentToken.java new file mode 100644 index 0000000..5685095 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/PersistentToken.java @@ -0,0 +1,129 @@ +package com.oguerreiro.resilient.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * Persistent tokens are used by Spring Security to automatically log in users. + * + * @see com.oguerreiro.resilient.security.PersistentTokenRememberMeServices + */ +@Entity +@Table(name = "jhi_persistent_token") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class PersistentToken implements Serializable { + + private static final long serialVersionUID = 1L; + + private static final int MAX_USER_AGENT_LEN = 255; + + @Id + private String series; + + @JsonIgnore + @NotNull + @Column(name = "token_value", nullable = false) + private String tokenValue; + + @Column(name = "token_date") + private LocalDate tokenDate; + + //an IPV6 address max length is 39 characters + @Size(min = 0, max = 39) + @Column(name = "ip_address", length = 39) + private String ipAddress; + + @Column(name = "user_agent") + private String userAgent; + + @JsonIgnore + @ManyToOne + private User user; + + public String getSeries() { + return series; + } + + public void setSeries(String series) { + this.series = series; + } + + public String getTokenValue() { + return tokenValue; + } + + public void setTokenValue(String tokenValue) { + this.tokenValue = tokenValue; + } + + public LocalDate getTokenDate() { + return tokenDate; + } + + public void setTokenDate(LocalDate tokenDate) { + this.tokenDate = tokenDate; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(String userAgent) { + if (userAgent.length() >= MAX_USER_AGENT_LEN) { + this.userAgent = userAgent.substring(0, MAX_USER_AGENT_LEN - 1); + } else { + this.userAgent = userAgent; + } + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PersistentToken)) { + return false; + } + return Objects.equals(series, ((PersistentToken) o).series); + } + + @Override + public int hashCode() { + return Objects.hashCode(series); + } + + // prettier-ignore + @Override + public String toString() { + return "PersistentToken{" + + "series='" + series + '\'' + + ", tokenValue='" + tokenValue + '\'' + + ", tokenDate=" + tokenDate + + ", ipAddress='" + ipAddress + '\'' + + ", userAgent='" + userAgent + '\'' + + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/ResilientLog.java b/src/main/java/com/oguerreiro/resilient/domain/ResilientLog.java new file mode 100644 index 0000000..391d94a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/ResilientLog.java @@ -0,0 +1,136 @@ +package com.oguerreiro.resilient.domain; + +import java.time.Instant; + +import com.oguerreiro.resilient.domain.enumeration.ResilientLogLevel; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Resilient Log + * Generic persistent log messages, to use in several processes + */ +@Entity +@Table(name = "resilient_log") +@ResilientSecurityResourceConfig() +public class ResilientLog extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + @NotNull + @Column(name = "owner_id", nullable = false) + private Long ownerId; + + @NotNull + @Column(name = "log_message", nullable = false) + private String logMessage; + + /** + * The creation date (insert). When the data was inserted into the system. + */ + @NotNull + @Column(name = "creation_date", nullable = false) + private Instant creationDate; + + /** + * The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login + * username. + */ + @NotNull + @Column(name = "creation_username", nullable = false) + private String creationUsername; + + /** + * Log Level + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "level", nullable = false) + private ResilientLogLevel level; + + public String getLogMessage() { + return this.logMessage; + } + + public ResilientLog logMessage(String logMessage) { + this.setLogMessage(logMessage); + return this; + } + + public void setLogMessage(String logMessage) { + this.logMessage = logMessage; + } + + public Instant getCreationDate() { + return this.creationDate; + } + + public ResilientLog creationDate(Instant creationDate) { + this.setCreationDate(creationDate); + return this; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return this.creationUsername; + } + + public ResilientLog creationUsername(String creationUsername) { + this.setCreationUsername(creationUsername); + return this; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public Long getOwnerId() { + return this.ownerId; + } + + public void setOwnerId(Long ownerId) { + this.ownerId = ownerId; + } + + public ResilientLog ownerId(Long ownerId) { + this.setOwnerId(ownerId); + return this; + } + + public ResilientLogLevel getLevel() { + return level; + } + + public ResilientLog level(ResilientLogLevel level) { + this.setLevel(level); + return this; + } + + public void setLevel(ResilientLogLevel level) { + this.level = level; + } + + //@formatter:off + @Override + public String toString() { + return "ResilientLog{" + + "id=" + getId() + + ", ownerId=" + getOwnerId() + + ", level=" + getLevel() + + ", logMessage='" + getLogMessage() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/Unit.java b/src/main/java/com/oguerreiro/resilient/domain/Unit.java new file mode 100644 index 0000000..cab4c9d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/Unit.java @@ -0,0 +1,160 @@ +package com.oguerreiro.resilient.domain; + +import java.math.BigDecimal; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Unit + */ +@Entity +@Table(name = "unit") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class Unit extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * Required code to use in integrations + */ + @NotNull + @Column(name = "code", nullable = false, unique = true) + private String code; + + /** + * Math symbol that is usually used as sufix for this unit. Eg. 'm3' for square metters. + */ + @NotNull + @Column(name = "symbol", nullable = false) + private String symbol; + + /** + * Math name for this unit. Eg. 'Square metters'. + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description") + private String description; + + /** + * The conversion rate (CR), relative to the base unit. + * This values answers the question: When the base unit is 1 (one), what's the value of this unit measure ? + * Ex: How many Ml (milligrams) 1Kg has? 1000 + * The formula to convert between any value unit the scale is: + * *CR(target unit)/CR(source unit) + */ + @NotNull + @Column(name = "convertion_rate", precision = 21, scale = 2, nullable = false) + private BigDecimal convertionRate; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "units" }, allowSetters = true) + private UnitType unitType; + + public String getCode() { + return this.code; + } + + public Unit code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getSymbol() { + return this.symbol; + } + + public Unit symbol(String symbol) { + this.setSymbol(symbol); + return this; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + public String getName() { + return this.name; + } + + public Unit name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public Unit description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public BigDecimal getConvertionRate() { + return this.convertionRate; + } + + public Unit convertionRate(BigDecimal convertionRate) { + this.setConvertionRate(convertionRate); + return this; + } + + public void setConvertionRate(BigDecimal convertionRate) { + this.convertionRate = convertionRate; + } + + public UnitType getUnitType() { + return this.unitType; + } + + public void setUnitType(UnitType unitType) { + this.unitType = unitType; + } + + public Unit unitType(UnitType unitType) { + this.setUnitType(unitType); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "Unit{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", symbol='" + getSymbol() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", convertionRate=" + getConvertionRate() + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/UnitConverter.java b/src/main/java/com/oguerreiro/resilient/domain/UnitConverter.java new file mode 100644 index 0000000..d72ebd9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/UnitConverter.java @@ -0,0 +1,127 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Unit Converter + */ +@Entity +@Table(name = "unit_converter") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class UnitConverter extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @NotNull + @Column(name = "description", nullable = false) + private String description; + + /** + * The convertion formula to convert the fromUnit into the toUnit. + */ + @NotNull + @Column(name = "convertion_formula", nullable = false) + private String convertionFormula; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "unitType" }, allowSetters = true) + private Unit fromUnit; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "unitType" }, allowSetters = true) + private Unit toUnit; + + public String getName() { + return this.name; + } + + public UnitConverter name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public UnitConverter description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getConvertionFormula() { + return this.convertionFormula; + } + + public UnitConverter convertionFormula(String convertionFormula) { + this.setConvertionFormula(convertionFormula); + return this; + } + + public void setConvertionFormula(String convertionFormula) { + this.convertionFormula = convertionFormula; + } + + public Unit getFromUnit() { + return this.fromUnit; + } + + public void setFromUnit(Unit unit) { + this.fromUnit = unit; + } + + public UnitConverter fromUnit(Unit unit) { + this.setFromUnit(unit); + return this; + } + + public Unit getToUnit() { + return this.toUnit; + } + + public void setToUnit(Unit unit) { + this.toUnit = unit; + } + + public UnitConverter toUnit(Unit unit) { + this.setToUnit(unit); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "UnitConverter{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", convertionFormula='" + getConvertionFormula() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/UnitType.java b/src/main/java/com/oguerreiro/resilient/domain/UnitType.java new file mode 100644 index 0000000..b4ca647 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/UnitType.java @@ -0,0 +1,129 @@ +package com.oguerreiro.resilient.domain; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * UnitType + * Type of measure unit types. Convertions can only be done within Unit's of the same UnitType + */ +@Entity +@Table(name = "unit_type") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class UnitType extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + @NotNull + @Column(name = "name", nullable = false, unique = true) + private String name; + + @NotNull + @Column(name = "description", nullable = false) + private String description; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "value_type", nullable = false) + private UnitValueType valueType; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "unitType") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "fromUnits", "toUnits", "baseUnits", "sourceUnits", "inputData", "outputData", + "variableUnits", "unitType" }, allowSetters = true) + private Set units = new HashSet<>(); + + public String getName() { + return this.name; + } + + public UnitType name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public UnitType description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public UnitValueType getValueType() { + return this.valueType; + } + + public UnitType valueType(UnitValueType valueType) { + this.setValueType(valueType); + return this; + } + + public void setValueType(UnitValueType valueType) { + this.valueType = valueType; + } + + public Set getUnits() { + return this.units; + } + + public void setUnits(Set units) { + if (this.units != null) { + this.units.forEach(i -> i.setUnitType(null)); + } + if (units != null) { + units.forEach(i -> i.setUnitType(this)); + } + this.units = units; + } + + public UnitType units(Set units) { + this.setUnits(units); + return this; + } + + public UnitType addUnit(Unit unit) { + this.units.add(unit); + unit.setUnitType(this); + return this; + } + + public UnitType removeUnit(Unit unit) { + this.units.remove(unit); + unit.setUnitType(null); + return this; + } + + // prettier-ignore + @Override + public String toString() { + return "UnitType{" + "id=" + getId() + ", name='" + getName() + "'" + ", description='" + getDescription() + "'" + + ", valueType='" + getValueType() + "'" + ", version=" + getVersion() + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/User.java b/src/main/java/com/oguerreiro/resilient/domain/User.java new file mode 100644 index 0000000..3b80242 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/User.java @@ -0,0 +1,267 @@ +package com.oguerreiro.resilient.domain; + +import java.io.Serializable; +import java.time.Instant; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.oguerreiro.resilient.config.Constants; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +/** + * A user. + */ +@Entity +@Table(name = "jhi_user") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class User extends AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + @Column(length = 50, unique = true, nullable = false) + private String login; + + @JsonIgnore + @NotNull + @Size(min = 60, max = 60) + @Column(name = "password_hash", length = 60, nullable = false) + private String password; + + @Size(max = 50) + @Column(name = "first_name", length = 50) + private String firstName; + + @Size(max = 50) + @Column(name = "last_name", length = 50) + private String lastName; + + @Email + @Size(min = 5, max = 254) + @Column(length = 254, unique = true) + private String email; + + @NotNull + @Column(nullable = false) + private boolean activated = false; + + @Size(min = 2, max = 10) + @Column(name = "lang_key", length = 10) + private String langKey; + + @Size(max = 256) + @Column(name = "image_url", length = 256) + private String imageUrl; + + @Size(max = 20) + @Column(name = "activation_key", length = 20) + @JsonIgnore + private String activationKey; + + @Size(max = 20) + @Column(name = "reset_key", length = 20) + @JsonIgnore + private String resetKey; + + @Column(name = "reset_date") + private Instant resetDate = null; + + @ManyToOne(fetch = FetchType.LAZY) + private SecurityGroup securityGroup; + + @JsonIgnore + @ManyToMany + @JoinTable(name = "jhi_user_authority", joinColumns = { + @JoinColumn(name = "user_id", referencedColumnName = "id") }, inverseJoinColumns = { + @JoinColumn(name = "authority_name", referencedColumnName = "name") }) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + @BatchSize(size = 20) + private Set authorities = new HashSet<>(); + + @JsonIgnore + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "user") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + private Set persistentTokens = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + // Lowercase the login before saving it in database + public void setLogin(String login) { + this.login = StringUtils.lowerCase(login, Locale.ENGLISH); + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getActivationKey() { + return activationKey; + } + + public void setActivationKey(String activationKey) { + this.activationKey = activationKey; + } + + public String getResetKey() { + return resetKey; + } + + public void setResetKey(String resetKey) { + this.resetKey = resetKey; + } + + public Instant getResetDate() { + return resetDate; + } + + public void setResetDate(Instant resetDate) { + this.resetDate = resetDate; + } + + public String getLangKey() { + return langKey; + } + + public void setLangKey(String langKey) { + this.langKey = langKey; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + public Set getPersistentTokens() { + return persistentTokens; + } + + public void setPersistentTokens(Set persistentTokens) { + this.persistentTokens = persistentTokens; + } + + public SecurityGroup getSecurityGroup() { + return securityGroup; + } + + public User securityGroup(SecurityGroup securityGroup) { + this.setSecurityGroup(securityGroup); + return this; + } + + public void setSecurityGroup(SecurityGroup securityGroup) { + this.securityGroup = securityGroup; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof User)) { + return false; + } + return id != null && id.equals(((User) o).id); + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "User{" + "login='" + login + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + ", imageUrl='" + imageUrl + '\'' + ", activated='" + activated + '\'' + + ", langKey='" + langKey + '\'' + ", activationKey='" + activationKey + '\'' + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/Variable.java b/src/main/java/com/oguerreiro/resilient/domain/Variable.java new file mode 100644 index 0000000..8cc3857 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/Variable.java @@ -0,0 +1,431 @@ +package com.oguerreiro.resilient.domain; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.enumeration.InputMode; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * Variable Definition (GEE variable classification) + */ +@Entity +@Table(name = "variable") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class Variable extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * Unique key, composed by:
+ * VariableScope.code + VariableCategory.code + + * NOTA: User will provide the index property, and the software will create the code + * index and code are NOT changeable + */ + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Column(name = "code", nullable = false, unique = true) + private String code; + + /** + * Variable index + */ + @Column(name = "variable_index", nullable = false) + private Integer variableIndex; + + /** + * The name or title for the variable + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + /** + * The descriptions of the variable. Its meaning. Its usage. + */ + @NotNull + @Column(name = "description", nullable = false) + private String description; + + /** + * Defines this variable as input. Values will be inserted for this variable + */ + @NotNull + @Column(name = "input", nullable = false) + private Boolean input; + + /** + * Defines the way of input. See the enum for instructions. + */ + @Enumerated(EnumType.STRING) + @Column(name = "input_mode", nullable = false) + private InputMode inputMode; + + /** + * Defines this variable as output. This variable will be evaluated for output + */ + @NotNull + @Column(name = "output", nullable = false) + private Boolean output; + + /** + * Defines this variable as output. This variable will be evaluated for output + */ + @NotNull + @Column(name = "output_single", nullable = true) + private Boolean outputSingle; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "user", "children", "parent", "organizationType" }, allowSetters = true) + private Organization outputOwner; + + /** + * The formula to calculate the output valut of the variable. Mandatory when output==TRUE + */ + @Column(name = "output_formula") + private String outputFormula; + + /** + * Record version for concurrency control. + */ + @Column(name = "value_scale") + private Integer valueScale; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "variable", cascade = CascadeType.ALL, orphanRemoval = true) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "variable", "unit" }, allowSetters = true) + private Set variableUnits = new HashSet<>(); + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "unitType" }, allowSetters = true) + private Unit baseUnit; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableCategories", "variables" }, allowSetters = true) + private VariableScope variableScope; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variables", "variableScope" }, allowSetters = true) + private VariableCategory variableCategory; + + @ManyToOne(fetch = FetchType.LAZY) + private VariableClassType variableClassType; + + @Column(name = "variable_class_label") + private String variableClassLabel; + + @Column(name = "variable_class_mandatory") + private Boolean variableClassMandatory; + + @Column(name = "hidden_for_main") + private Boolean hiddenForMain; + + public String getCode() { + return this.code; + } + + public Variable code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public Integer getVariableIndex() { + return this.variableIndex; + } + + public Variable variableIndex(Integer variableIndex) { + this.setVariableIndex(variableIndex); + return this; + } + + public void setVariableIndex(Integer variableIndex) { + this.variableIndex = variableIndex; + } + + public String getName() { + return this.name; + } + + public Variable name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public Variable description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getInput() { + return this.input; + } + + public Variable input(Boolean input) { + this.setInput(input); + return this; + } + + public void setInput(Boolean input) { + this.input = input; + } + + public InputMode getInputMode() { + return this.inputMode; + } + + public Variable inputMode(InputMode inputMode) { + this.setInputMode(inputMode); + return this; + } + + public void setInputMode(InputMode inputMode) { + this.inputMode = inputMode; + } + + public Boolean getOutput() { + return this.output; + } + + public Variable output(Boolean output) { + this.setOutput(output); + return this; + } + + public void setOutput(Boolean output) { + this.output = output; + } + + public Boolean getOutputSingle() { + return this.outputSingle; + } + + public Variable outputSingle(Boolean outputSingle) { + this.setOutputSingle(outputSingle); + return this; + } + + public void setOutputSingle(Boolean outputSingle) { + this.outputSingle = outputSingle; + } + + public String getOutputFormula() { + return this.outputFormula; + } + + public Variable outputFormula(String outputFormula) { + this.setOutputFormula(outputFormula); + return this; + } + + public void setOutputFormula(String outputFormula) { + this.outputFormula = outputFormula; + } + + public Integer getValueScale() { + return this.valueScale; + } + + public Variable valueScale(Integer valueScale) { + this.setValueScale(valueScale); + return this; + } + + public void setValueScale(Integer valueScale) { + this.valueScale = valueScale; + } + + public boolean isUnitAllowed(Unit unit) { + return this.getVariableUnits().stream().anyMatch(varUnit -> varUnit.getUnit().equals(unit)) + || this.getBaseUnit().getUnitType().equals(unit.getUnitType()); + } + + public Set getVariableUnits() { + return this.variableUnits; + } + + public void setVariableUnits(Set variableUnits) { + if (this.variableUnits != null) { + this.variableUnits.forEach(i -> i.setVariable(null)); + } + if (variableUnits != null) { + variableUnits.forEach(i -> i.setVariable(this)); + } + this.variableUnits = variableUnits; + } + + public Variable variableUnits(Set variableUnits) { + this.setVariableUnits(variableUnits); + return this; + } + + public Variable addVariableUnits(VariableUnits variableUnits) { + this.variableUnits.add(variableUnits); + variableUnits.setVariable(this); + return this; + } + + public Variable removeVariableUnits(VariableUnits variableUnits) { + this.variableUnits.remove(variableUnits); + variableUnits.setVariable(null); + return this; + } + + public Organization getOutputOwner() { + return this.outputOwner; + } + + public void setOutputOwner(Organization outputOwner) { + this.outputOwner = outputOwner; + } + + public Variable outputOwner(Organization outputOwner) { + this.setOutputOwner(outputOwner); + return this; + } + + public Unit getBaseUnit() { + return this.baseUnit; + } + + public void setBaseUnit(Unit unit) { + this.baseUnit = unit; + } + + public Variable baseUnit(Unit unit) { + this.setBaseUnit(unit); + return this; + } + + public VariableScope getVariableScope() { + return this.variableScope; + } + + public void setVariableScope(VariableScope variableScope) { + this.variableScope = variableScope; + } + + public Variable variableScope(VariableScope variableScope) { + this.setVariableScope(variableScope); + return this; + } + + public VariableCategory getVariableCategory() { + return this.variableCategory; + } + + public void setVariableCategory(VariableCategory variableCategory) { + this.variableCategory = variableCategory; + } + + public Variable variableCategory(VariableCategory variableCategory) { + this.setVariableCategory(variableCategory); + return this; + } + + public VariableClassType getVariableClassType() { + return this.variableClassType; + } + + public void setVariableClassType(VariableClassType variableClassType) { + this.variableClassType = variableClassType; + } + + public Variable variableClassType(VariableClassType variableClassType) { + this.setVariableClassType(variableClassType); + return this; + } + + public String getVariableClassLabel() { + return variableClassLabel; + } + + public void setVariableClassLabel(String variableClassLabel) { + this.variableClassLabel = variableClassLabel; + } + + public Variable variableClassLabel(String variableClassLabel) { + this.setVariableClassLabel(variableClassLabel); + return this; + } + + public Boolean getVariableClassMandatory() { + return variableClassMandatory; + } + + public void setVariableClassMandatory(Boolean variableClassMandatory) { + this.variableClassMandatory = variableClassMandatory; + } + + public Variable variableClassMandatory(Boolean variableClassMandatory) { + this.setVariableClassMandatory(variableClassMandatory); + return this; + } + + public Boolean getHiddenForMain() { + return hiddenForMain; + } + + public void setHiddenForMain(Boolean hiddenForMain) { + this.hiddenForMain = hiddenForMain; + } + + public Variable hiddenForMain(Boolean hiddenForMain) { + this.setHiddenForMain(hiddenForMain); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "Variable{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", variableIndex=" + getVariableIndex() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", input='" + getInput() + "'" + + ", inputMode='" + getInputMode() + "'" + + ", output='" + getOutput() + "'" + + ", outputSingle='" + getOutputSingle() + "'" + + ", outputFormula='" + getOutputFormula() + "'" + + ", valueScale=" + getValueScale() + + ", hiddenForMain=" + getHiddenForMain() + + " , version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/VariableCategory.java b/src/main/java/com/oguerreiro/resilient/domain/VariableCategory.java new file mode 100644 index 0000000..96f3ec6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/VariableCategory.java @@ -0,0 +1,194 @@ +package com.oguerreiro.resilient.domain; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * Variable Category + * Sub-division of VariableScope in Categories + */ +@Entity +@Table(name = "variable_category") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class VariableCategory extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * Unique key, user defined. + * Be aware, that this will be used to create the Variable.code + * NOTE: The field type is string, but restricted to only numbers + */ + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Column(name = "code", nullable = false) + private String code; + + /** + * The name or title for this category + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + /** + * A description or usage of this category + */ + @NotNull + @Column(name = "description", nullable = false) + private String description; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "variableCategory") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "variableClasses", "inputData", "outputData", "variableUnits", "baseUnit", + "variableScope", "variableCategory" }, allowSetters = true) + private Set variables = new HashSet<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableCategories", "variables" }, allowSetters = true) + private VariableScope variableScope; + + @Column(name = "hidden_for_data") + private Boolean hiddenForData; + + @Column(name = "hidden_for_factor") + private Boolean hiddenForFactor; + + public String getCode() { + return this.code; + } + + public VariableCategory code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public VariableCategory name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public VariableCategory description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getVariables() { + return this.variables; + } + + public void setVariables(Set variables) { + if (this.variables != null) { + this.variables.forEach(i -> i.setVariableCategory(null)); + } + if (variables != null) { + variables.forEach(i -> i.setVariableCategory(this)); + } + this.variables = variables; + } + + public VariableCategory variables(Set variables) { + this.setVariables(variables); + return this; + } + + public VariableCategory addVariable(Variable variable) { + this.variables.add(variable); + variable.setVariableCategory(this); + return this; + } + + public VariableCategory removeVariable(Variable variable) { + this.variables.remove(variable); + variable.setVariableCategory(null); + return this; + } + + public VariableScope getVariableScope() { + return this.variableScope; + } + + public void setVariableScope(VariableScope variableScope) { + this.variableScope = variableScope; + } + + public VariableCategory variableScope(VariableScope variableScope) { + this.setVariableScope(variableScope); + return this; + } + + public Boolean getHiddenForData() { + return hiddenForData; + } + + public void setHiddenForData(Boolean hiddenForData) { + this.hiddenForData = hiddenForData; + } + + public VariableCategory hiddenForData(Boolean hiddenForData) { + this.setHiddenForData(hiddenForData); + return this; + } + + public Boolean getHiddenForFactor() { + return hiddenForFactor; + } + + public void setHiddenForFactor(Boolean hiddenForFactor) { + this.hiddenForFactor = hiddenForFactor; + } + + public VariableCategory hiddenForFactor(Boolean hiddenForFactor) { + this.setHiddenForFactor(hiddenForFactor); + return this; + } + + //@formatter:off + @Override + public String toString() { + return "VariableCategory{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/VariableClass.java b/src/main/java/com/oguerreiro/resilient/domain/VariableClass.java new file mode 100644 index 0000000..f3ab60d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/VariableClass.java @@ -0,0 +1,97 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * Variable Class + * A sub-variable classification. Example: Variable=FUEL CONSUMPTIOM; CLASS=GASOLINE; OR ELECTRICAL; ETC... + */ +@Entity +@Table(name = "variable_class") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class VariableClass extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * Code of the class. Must be unique within the Variavle + */ + @NotNull + @Column(name = "code", nullable = false) + private String code; + + /** + * Human name of the class. Must be unique within the Variable, because it will be used as key for the Excel + * integration data. + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableClasses" }, allowSetters = true) + private VariableClassType variableClassType; + + public String getCode() { + return this.code; + } + + public VariableClass code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public VariableClass name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public VariableClassType getVariableClassType() { + return this.variableClassType; + } + + public void setVariableClassType(VariableClassType variableClassType) { + this.variableClassType = variableClassType; + } + + public VariableClass variableClassType(VariableClassType variableClassType) { + this.setVariableClassType(variableClassType); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "VariableClass{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/VariableClassType.java b/src/main/java/com/oguerreiro/resilient/domain/VariableClassType.java new file mode 100644 index 0000000..ef8af5e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/VariableClassType.java @@ -0,0 +1,153 @@ +package com.oguerreiro.resilient.domain; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * VariableClassType + * Type of a list of Variable Classe's + */ +@Entity +@Table(name = "variable_class_type") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class VariableClassType extends AbstractResilientLongIdEntity { + private static final long serialVersionUID = 1L; + + /** + * User defined code. Must be unique + */ + @NotNull + @Size(min = 1) + @Column(name = "code", nullable = false, unique = true) + private String code; + + @NotNull + @Column(name = "name", nullable = false, unique = true) + private String name; + + @NotNull + @Column(name = "description", nullable = false) + private String description; + + @NotNull + @Column(name = "label", nullable = false, unique = true) + private String label; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "variableClassType", cascade = CascadeType.ALL, orphanRemoval = true) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "variableClassType" }, allowSetters = true) + private Set variableClasses = new HashSet<>(); + + public String getCode() { + return this.code; + } + + public VariableClassType code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public VariableClassType name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public VariableClassType description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLabel() { + return this.label; + } + + public VariableClassType label(String label) { + this.setLabel(label); + return this; + } + + public void setLabel(String label) { + this.label = label; + } + + public Set getVariableClasses() { + return this.variableClasses; + } + + public void setVariableClasses(Set variableClasses) { + if (this.variableClasses != null) { + this.variableClasses.forEach((v) -> v.setVariableClassType(null)); + } + if (variableClasses != null) { + variableClasses.forEach((v) -> v.setVariableClassType(this)); + } + this.variableClasses = variableClasses; + } + + public VariableClassType variableClasses(Set variableClasses) { + this.setVariableClasses(variableClasses); + return this; + } + + public VariableClassType addVariableClass(VariableClass variableClass) { + this.variableClasses.add(variableClass); + variableClass.setVariableClassType(this); + return this; + } + + public VariableClassType removeVariableClass(VariableClass variableClass) { + this.variableClasses.remove(variableClass); + variableClass.setVariableClassType(null); + return this; + } + +//@formatter:off + @Override + public String toString() { + return "VariableClassType{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", label='" + getLabel() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/VariableScope.java b/src/main/java/com/oguerreiro/resilient/domain/VariableScope.java new file mode 100644 index 0000000..67c818f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/VariableScope.java @@ -0,0 +1,212 @@ +package com.oguerreiro.resilient.domain; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * Variable Scope + * Example: GEE Scope 1/2/3 or General Info + */ +@Entity +@Table(name = "variable_scope") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class VariableScope extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + /** + * Unique key, user defined. + * Be aware, that this will be used to create the Variable.code + * NOTE: The field type is string, but restricted to only numbers + */ + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Column(name = "code", nullable = false, unique = true) + private String code; + + /** + * The name or title for this scope + */ + @NotNull + @Column(name = "name", nullable = false) + private String name; + + /** + * A description or usage of this scope + */ + @NotNull + @Column(name = "description", nullable = false) + private String description; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "variableScope") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "variables", "variableScope" }, allowSetters = true) + private Set variableCategories = new HashSet<>(); + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "variableScope") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "variableClasses", "inputData", "outputData", "variableUnits", "baseUnit", + "variableScope", "variableCategory" }, allowSetters = true) + private Set variables = new HashSet<>(); + + @Column(name = "hidden_for_data") + private Boolean hiddenForData; + + @Column(name = "hidden_for_factor") + private Boolean hiddenForFactor; + + public String getCode() { + return this.code; + } + + public VariableScope code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return this.name; + } + + public VariableScope name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public VariableScope description(String description) { + this.setDescription(description); + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getVariableCategories() { + return this.variableCategories; + } + + public void setVariableCategories(Set variableCategories) { + if (this.variableCategories != null) { + this.variableCategories.forEach(i -> i.setVariableScope(null)); + } + if (variableCategories != null) { + variableCategories.forEach(i -> i.setVariableScope(this)); + } + this.variableCategories = variableCategories; + } + + public VariableScope variableCategories(Set variableCategories) { + this.setVariableCategories(variableCategories); + return this; + } + + public VariableScope addVariableCategory(VariableCategory variableCategory) { + this.variableCategories.add(variableCategory); + variableCategory.setVariableScope(this); + return this; + } + + public VariableScope removeVariableCategory(VariableCategory variableCategory) { + this.variableCategories.remove(variableCategory); + variableCategory.setVariableScope(null); + return this; + } + + public Set getVariables() { + return this.variables; + } + + public void setVariables(Set variables) { + if (this.variables != null) { + this.variables.forEach(i -> i.setVariableScope(null)); + } + if (variables != null) { + variables.forEach(i -> i.setVariableScope(this)); + } + this.variables = variables; + } + + public VariableScope variables(Set variables) { + this.setVariables(variables); + return this; + } + + public VariableScope addVariable(Variable variable) { + this.variables.add(variable); + variable.setVariableScope(this); + return this; + } + + public VariableScope removeVariable(Variable variable) { + this.variables.remove(variable); + variable.setVariableScope(null); + return this; + } + + public Boolean getHiddenForData() { + return this.hiddenForData; + } + + public VariableScope hiddenForData(Boolean hiddenForData) { + this.setHiddenForData(hiddenForData); + return this; + } + + public void setHiddenForData(Boolean hiddenForData) { + this.hiddenForData = hiddenForData; + } + + public Boolean getHiddenForFactor() { + return this.hiddenForFactor; + } + + public VariableScope hiddenForFactor(Boolean hiddenForFactor) { + this.setHiddenForFactor(hiddenForFactor); + return this; + } + + public void setHiddenForFactor(Boolean hiddenForFactor) { + this.hiddenForFactor = hiddenForFactor; + } + +//@formatter:off + @Override + public String toString() { + return "VariableScope{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/VariableUnits.java b/src/main/java/com/oguerreiro/resilient/domain/VariableUnits.java new file mode 100644 index 0000000..9ac89ee --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/VariableUnits.java @@ -0,0 +1,82 @@ +package com.oguerreiro.resilient.domain; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.security.custom.ResilientSecurityResourceConfig; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +/** + * Variable allowed input units. Also defines the converters needed if the + */ +@Entity +@Table(name = "variable_units") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@ResilientSecurityResourceConfig() +public class VariableUnits extends AbstractResilientLongIdEntity { + + private static final long serialVersionUID = 1L; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "variableUnits", "baseUnit", "variableScope", + "variableCategory" }, allowSetters = true) + private Variable variable; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "unitType" }, allowSetters = true) + private Unit unit; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "unitType", "fromUnit", "toUnit" }, allowSetters = true) + private UnitConverter unitConverter; + + public Variable getVariable() { + return this.variable; + } + + public void setVariable(Variable variable) { + this.variable = variable; + } + + public VariableUnits variable(Variable variable) { + this.setVariable(variable); + return this; + } + + public Unit getUnit() { + return this.unit; + } + + public void setUnit(Unit unit) { + this.unit = unit; + } + + public VariableUnits unit(Unit unit) { + this.setUnit(unit); + return this; + } + + public UnitConverter getUnitConverter() { + return this.unitConverter; + } + + public void setUnitConverter(UnitConverter unitConverter) { + this.unitConverter = unitConverter; + } + + public VariableUnits unitConverter(UnitConverter unitConverter) { + this.setUnitConverter(unitConverter); + return this; + } + + // prettier-ignore + @Override + public String toString() { + return "VariableUnits{" + "id=" + getId() + ", version=" + getVersion() + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/ContentPageType.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/ContentPageType.java new file mode 100644 index 0000000..83e1a5f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/ContentPageType.java @@ -0,0 +1,21 @@ +package com.oguerreiro.resilient.domain.enumeration; + +/** + * Content Page Type Enum
+ * Defines the 'slug' of a page + *
    + *
  • {@link #HOME_ANONYMOUS}
  • + *
  • {@link #HOME_AUTHENTICATED}
  • + *
+ */ +public enum ContentPageType { + /** + * The Home page presented to anonymous users + */ + HOME_ANONYMOUS, + + /** + * The Home page presented to authenticated users + */ + HOME_AUTHENTICATED, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentChartType.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentChartType.java new file mode 100644 index 0000000..f66577d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentChartType.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.domain.enumeration; + +public enum DashboardComponentChartType { + /** + * Bar chart type + */ + BAR, + /** + * Pie chart type + */ + PIE, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentType.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentType.java new file mode 100644 index 0000000..8d52975 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentType.java @@ -0,0 +1,18 @@ +package com.oguerreiro.resilient.domain.enumeration; + +public enum DashboardComponentType { + /** + * The component is presented as a table + */ + TABLE, + /** + * The component is presented as a accordion-table + */ + ACCORDION, + /** + * The component is presented as a chart. + * + * @see {@link DashboardComponentChartType} + */ + CHART, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentView.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentView.java new file mode 100644 index 0000000..ca8b273 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DashboardComponentView.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.domain.enumeration; + +public enum DashboardComponentView { + /** + * The component is presented in Emissions page + */ + EMISSIONS, + /** + * The component is presented in Indicators page + */ + INDICATORS, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/DataSourceType.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DataSourceType.java new file mode 100644 index 0000000..d20c73d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/DataSourceType.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.domain.enumeration; + +/** + * Data Source enum + */ +public enum DataSourceType { + MANUAL, + FILE, + AUTO, + /** + * Special case, where a InputData value from an Organization is distributed to other Organization's + */ + DISTRIB, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/InputMode.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/InputMode.java new file mode 100644 index 0000000..855102c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/InputMode.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.domain.enumeration; + +/** + * Variable InputMode enum + * Defines how the input values for the variable are inserted. + */ +public enum InputMode { + /** + * Values are inserted by DataFile only. User can always change the value manually, but they can't delete it or insert it + */ + DATAFILE, + /** + * Values are inserted Manually. In this mode, only a human can insert and change the variable values. If a file or integration attempts to insert it, an error will be thrown + */ + MANUAL, + /** + * Values are inserted by Integration files. Very similar to the option DATAFILE + */ + INTEGRATION, + /** + * The variable can be inserted by any of the modes + */ + ANY, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/MetadataType.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/MetadataType.java new file mode 100644 index 0000000..a53ab37 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/MetadataType.java @@ -0,0 +1,13 @@ +package com.oguerreiro.resilient.domain.enumeration; + +/** + * Metadata type enum + * The property type + */ +public enum MetadataType { + STRING, + BOOLEAN, + INTEGER, + DECIMAL, + DATE, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/OrganizationNature.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/OrganizationNature.java new file mode 100644 index 0000000..f935937 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/OrganizationNature.java @@ -0,0 +1,40 @@ +package com.oguerreiro.resilient.domain.enumeration; + +/** + * Organization Nature enum + * This identifies the nature of the organization entry level. + */ +public enum OrganizationNature { + /** + * + * Represents an organizational level (company; department; university; ...) + * Rules : An Organization always starts with and entry level ORGANIZATION. + * Parent's allowed: none; ORGANIZATION; LEVEL + + */ + ORGANIZATION, + /** + * + * Represents a human. With or without a login to the APP. + * Rules : Must have a parent Organization entry + * Parent's allowed: any + + */ + PERSON, + /** + * + * Represents physical building. + * Rules : Must have a parent Organization entry + * Parent's allowed: ORGANIZATION; LEVEL + + */ + FACILITY, + /** + * + * Generic. Its a organizer, creating a level without any system meaning + * Rules : Must have a parent Organization entry + * Parent's allowed: any + + */ + LEVEL, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/PeriodStatus.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/PeriodStatus.java new file mode 100644 index 0000000..3e286a8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/PeriodStatus.java @@ -0,0 +1,82 @@ +package com.oguerreiro.resilient.domain.enumeration; + +import com.oguerreiro.resilient.activities.period.close.PeriodCloseActivity; +import com.oguerreiro.resilient.activities.period.open.PeriodOpenActivity; +import com.oguerreiro.resilient.activities.period.process.PeriodProcessOutputsActivity; +import com.oguerreiro.resilient.activities.period.reopen.PeriodReopenActivity; +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.domain.PeriodVersion; + +/** + * Period Status Enum
+ * Enumeration of possible {@link Period} states. This states are set + * automatically by the {@link Activity Activity engine} + *
    + *
  • {@link #CREATED}
  • + *
  • {@link #OPENED}
  • + *
  • {@link #CLOSED}
  • + *
  • {@link #PROCESSING}
  • + *
  • {@link #ENDED}
  • + *
+ * Standard expected state transitions:
+ * {@link #CREATED} » {@link #OPENED} » {@link #CLOSED} » {@link #PROCESSING} » + * {@link #ENDED}
+ *
+ * Transitions allowed: + *
    + *
  • CREATED » OPENED - Manual activity {@link PeriodOpenActivity Open})
  • + *
  • OPENED » CLOSED - Manual activity {@link PeriodCloseActivity Close})
  • + *
  • CLOSED » PROCESSING - Manual activity {@link PeriodProcessOutputsActivity + * Process Outputs})
  • + *
  • PROCESSING » ENDED - Automatically. Changed by activity + * {@link PeriodProcessOutputsActivity Process Outputs}), if successful.
  • + *
  • ENDED * OPEN - Manual activity {@link PeriodReopenActivity Reopen})
  • + *
+ * + */ +public enum PeriodStatus { + /** + * Default when the Period is created.
+ * Activities allowed: + *
    + *
  • Open - Changes Period State to {@link #OPENED}
  • + *
+ */ + CREATED, + + /** + * The Period is ready for data insertion values (InputData). It's open for data + * input. Activities allowed: + *
    + *
  • Close - Changes Period State to {@link #CLOSED}
  • + *
+ */ + OPENED, + /** + * The Period is closed for data input. No more info can be inserted into the + * period. Activities allowed: + *
    + *
  • Process Outputs - Changes Period State to {@link #PROCESSING}. Will + * create a new {@link PeriodVersion} and do the actual Output generation.
  • + *
+ */ + CLOSED, + /** + * The Period is calculating output's. Activities allowed: + *
    + *
  • End - Changes Period State to {@link #ENDED}. This is done internally by + * the PeriodVersion, when it finishes the processing without error's.
  • + *
+ */ + PROCESSING, + /** + * The Period has finished. This means that it has, at least, one PeriodVersion + * created and fully processed with generated outputs. This is considered a + * terminal state. Activities allowed: + *
    + *
  • Reopen - Changes Period State to {@link #OPEN}. This allows for InputData + * corrections. The Period must be processed again.
  • + *
+ */ + ENDED, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/PeriodVersionStatus.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/PeriodVersionStatus.java new file mode 100644 index 0000000..cadb745 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/PeriodVersionStatus.java @@ -0,0 +1,58 @@ +package com.oguerreiro.resilient.domain.enumeration; + +import com.oguerreiro.resilient.activities.period.process.PeriodProcessOutputsActivity; +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.domain.PeriodVersion; + +/** + * Period Version Status Enum
+ * Enumeration of possible {@link PeriodVersion} states. This states are set + * automatically by the {@link Activity Activity engine} + *
    + *
  • {@link #PROCESSING}
  • + *
  • {@link #ERROR}
  • + *
  • {@link #ENDED}
  • + *
+ * Standard expected state transitions:
+ * {@link #PROCESSING} » {@link #ENDED}
+ *
+ * Transitions allowed: + *
    + *
  • PROCESSING - Startup state. The {@link PeriodVersion} is created to + * process outputs, and so, it's starts with PROCESSING state.
  • + *
  • PROCESSING » ERROR - Automatically. Changed by activity + * {@link PeriodVersionProcessOutputsActivity Process} if an error occurs
  • + *
  • PROCESSING » ENDED - Automatically, when invoked by Period activity + * {@link PeriodProcessOutputsActivity Process Outputs}. Or manually, by calling + * {@link PeriodVersionProcessOutputsActivity Process}. In both cases, only if + * processed without error. See the state {@link #ERROR}
  • + *
+ * + */ +public enum PeriodVersionStatus { + /** + * Default when the PeriodVersion is created.
+ * The PeriodVersion is calculating output's. No Activities allowed, this means + * that an activity is running. + */ + PROCESSING, + + /** + * An error occurred when the PeriodVersion was calculating output's. Activities + * allowed: + *
    + *
  • Process Outputs - Re-attempts to process outputs. Calls activity + * {@link PeriodVersionProcessOutputsActivity Process}.
  • + *
+ */ + ERROR, + + /** + * The PeriodVersion has finished. This means that all configured output + * variables where calculated and the {@link OutputData} where created. This is + * considered a terminal state.
+ * No activities are allowed. + */ + ENDED, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/ResilientLogLevel.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/ResilientLogLevel.java new file mode 100644 index 0000000..a8c7425 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/ResilientLogLevel.java @@ -0,0 +1,32 @@ +package com.oguerreiro.resilient.domain.enumeration; + +import com.oguerreiro.resilient.domain.ResilientLog; + +/** + * Resilient Log Level Enum
+ * The level of the log associated with the log message {@link ResilientLog} + *
    + *
  • {@link #INFO}
  • + *
  • {@link #WARN}
  • + *
  • {@link #ERROR}
  • + *
+ * + */ +public enum ResilientLogLevel { + /** + * Default, if no level defined. Use this just to log information about the ongoing process. + */ + INFO, + + /** + * Warning messages occurred within the process. Use this to tell users aspects of improvement, or suspicious things + * that should be better evaluated. + */ + WARN, + + /** + * Errors. Definitely something the user MUST act. Use this to give better knowledge of the occurred error and, if + * possible, the actions that the user should do. + */ + ERROR, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/UnitValueType.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/UnitValueType.java new file mode 100644 index 0000000..4419a02 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/UnitValueType.java @@ -0,0 +1,49 @@ +package com.oguerreiro.resilient.domain.enumeration; + +import java.math.BigDecimal; + +/** + * Unit Variable Type + * The type of data this unit represents + */ +public enum UnitValueType { + /** + * String value + */ + STRING, + /** + * Decimal value + */ + DECIMAL, + /** + * boolean value + */ + BOOLEAN; + + public boolean isValueValid(String value) { + try { + switch (this) { + case STRING: { + String.valueOf(value); + break; + } + case BOOLEAN: { + Boolean.valueOf(value); + break; + } + case DECIMAL: { + new BigDecimal(value); + break; + } + default: { + // Unexpected UnitValueType + return false; + } + } + } catch (Exception ex) { + return false; + } + + return true; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/UploadStatus.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/UploadStatus.java new file mode 100644 index 0000000..c7e1dd0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/UploadStatus.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.domain.enumeration; + +/** + * Upload file status enum + */ +public enum UploadStatus { + UPLOADED, + PROCESSING, + PROCESSED, + ERROR, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/UploadType.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/UploadType.java new file mode 100644 index 0000000..6a8078b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/UploadType.java @@ -0,0 +1,31 @@ +package com.oguerreiro.resilient.domain.enumeration; + +/** + * Upload file type enum + */ +public enum UploadType { + /** + * Custom Excel file template, for variable input upload + */ + INVENTORY, + + /** + * Custom Excel file template, for variable input upload of ENERGY data (same template of GLOBAL) + */ + ENERGY, + + /** + * Custom Excel file template, for variable input upload of GAS data (same template of GLOBAL) + */ + GAS, + + /** + * Custom Excel file template, for variable input upload of GAS data + */ + GLOBAL, + + /** + * Exported file from the accounting software + */ + ACCOUNTING, +} diff --git a/src/main/java/com/oguerreiro/resilient/domain/enumeration/package-info.java b/src/main/java/com/oguerreiro/resilient/domain/enumeration/package-info.java new file mode 100644 index 0000000..8ec8072 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/enumeration/package-info.java @@ -0,0 +1,4 @@ +/** + * This package file was generated by JHipster + */ +package com.oguerreiro.resilient.domain.enumeration; diff --git a/src/main/java/com/oguerreiro/resilient/domain/package-info.java b/src/main/java/com/oguerreiro/resilient/domain/package-info.java new file mode 100644 index 0000000..3743bbe --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/package-info.java @@ -0,0 +1,4 @@ +/** + * Domain objects. + */ +package com.oguerreiro.resilient.domain; diff --git a/src/main/java/com/oguerreiro/resilient/domain/usertype/ActivityProgressUserType.java b/src/main/java/com/oguerreiro/resilient/domain/usertype/ActivityProgressUserType.java new file mode 100644 index 0000000..86421de --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/domain/usertype/ActivityProgressUserType.java @@ -0,0 +1,97 @@ +package com.oguerreiro.resilient.domain.usertype; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Properties; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.ParameterizedType; +import org.hibernate.usertype.UserType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor; +import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry; + +@Component +public class ActivityProgressUserType implements UserType, ParameterizedType { + + @Autowired + private ActivityProgressRegistry activityProgressRegistry; + + private String domainClassName; + + @Override + public int getSqlType() { + return Types.VARCHAR; + } + + @Override + public Class returnedClass() { + return String.class; + } + + @Override + public boolean equals(String x, String y) { + return x == y || (x != null && x.equals(y)); + } + + @Override + public int hashCode(String x) { + return x.hashCode(); + } + + @Override + public String nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) + throws SQLException { + String id = rs.getString(position); + + if (id == null || id.trim().length() == 0) + return null; + + ActivityProgressDescriptor apd = this.activityProgressRegistry.progressOf(this.domainClassName + "@" + id); + if (apd == null) + return null; + + if (apd.getProgressPercentage() >= 100) + return null; + + return apd.getProgressPercentage().toString(); + } + + @Override + public void nullSafeSet(PreparedStatement st, String value, int index, SharedSessionContractImplementor session) + throws SQLException { + // TODO Auto-generated method stub + // IGNORE. This is a virtual property READONLY. + } + + @Override + public String deepCopy(String value) { + return value; + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble(String value) { + return (Serializable) deepCopy(value); + } + + @Override + public String assemble(Serializable cached, Object owner) { + return (String) cached; + } + + @Override + public void setParameterValues(Properties parameters) { + this.domainClassName = parameters.getProperty("activityDomainClass"); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/ExpressionEvaluationException.java b/src/main/java/com/oguerreiro/resilient/error/ExpressionEvaluationException.java new file mode 100644 index 0000000..b28cca5 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ExpressionEvaluationException.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.error; + +public class ExpressionEvaluationException extends ResilientException { + private static final String CODE = "E008"; + + public ExpressionEvaluationException(String expression, Throwable cause) { + super("An error occurred evaluating the expression: [" + expression + "]. With the following error: " + + cause.getMessage()); + + addMessageArg(expression); + addMessageArg(cause.getMessage()); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/ImporterOrganizationCodeNotFoundException.java b/src/main/java/com/oguerreiro/resilient/error/ImporterOrganizationCodeNotFoundException.java new file mode 100644 index 0000000..5fd2808 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ImporterOrganizationCodeNotFoundException.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.error; + +public class ImporterOrganizationCodeNotFoundException extends ResilientException { + private static final String CODE = "E014"; + + public ImporterOrganizationCodeNotFoundException(String sheetName, int row, String organizationCode) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Organization Code [" + organizationCode + + "]. Organization not found."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(organizationCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} \ No newline at end of file diff --git a/src/main/java/com/oguerreiro/resilient/error/ImporterOrganizationNotAccessibleException.java b/src/main/java/com/oguerreiro/resilient/error/ImporterOrganizationNotAccessibleException.java new file mode 100644 index 0000000..86d50e8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ImporterOrganizationNotAccessibleException.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.error; + +public class ImporterOrganizationNotAccessibleException extends ResilientException { + private static final String CODE = "E015"; + + public ImporterOrganizationNotAccessibleException(String sheetName, int row, String variableCode, + String inputOrganizationCode, String importerOrganizationCode) { + super("Sheet [" + sheetName + "]; Row [" + row + "]; Variable [" + variableCode + "]; Input Org. Code [" + + inputOrganizationCode + "]; Importer Org. Code [" + importerOrganizationCode + + "]. Input Organization must be a children of the Importer Organization."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(inputOrganizationCode); + addMessageArg(importerOrganizationCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} \ No newline at end of file diff --git a/src/main/java/com/oguerreiro/resilient/error/ImporterPeriodNotOpenedToReceiveDataRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/ImporterPeriodNotOpenedToReceiveDataRuntimeException.java new file mode 100644 index 0000000..aaad7f0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ImporterPeriodNotOpenedToReceiveDataRuntimeException.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.error; + +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; + +public class ImporterPeriodNotOpenedToReceiveDataRuntimeException extends ResilientRuntimeException { + private static final String CODE = "E023"; + + public ImporterPeriodNotOpenedToReceiveDataRuntimeException(String name, PeriodStatus currentState) { + super("Period [" + name + "] not ready to receive data. Expected state " + PeriodStatus.OPENED + + " but it's current state is " + currentState + "."); + + addMessageArg(name); + addMessageArg(PeriodStatus.OPENED); + addMessageArg(currentState); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} \ No newline at end of file diff --git a/src/main/java/com/oguerreiro/resilient/error/ImporterVariableValueException.java b/src/main/java/com/oguerreiro/resilient/error/ImporterVariableValueException.java new file mode 100644 index 0000000..e1ab657 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ImporterVariableValueException.java @@ -0,0 +1,23 @@ +package com.oguerreiro.resilient.error; + +public class ImporterVariableValueException extends ResilientException { + private static final String CODE = "E011"; + + public ImporterVariableValueException(String sheetName, int row, String variableCode, String variableType, + String variableValue) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Variable Code [" + variableCode + + "]; Unable to read the value [" + variableValue + "] has type [" + variableType + "]."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(variableType); + addMessageArg(variableValue); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} \ No newline at end of file diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportOrganizationDontMatchRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportOrganizationDontMatchRuntimeException.java new file mode 100644 index 0000000..078c267 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportOrganizationDontMatchRuntimeException.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportOrganizationDontMatchRuntimeException extends ResilientRuntimeException { + private static final String CODE = "E024"; + + public InventoryImportOrganizationDontMatchRuntimeException(String inputDataUploadOrganizationCode, String uoCode) { + super( + "The InputDataUpload configured organization is NOT the same UO within the file. The configured organization code is [" + + inputDataUploadOrganizationCode + "] and the file contains [" + uoCode + "]."); + + addMessageArg(inputDataUploadOrganizationCode); + addMessageArg(uoCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableClassNotFoundException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableClassNotFoundException.java new file mode 100644 index 0000000..97d8a93 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableClassNotFoundException.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableClassNotFoundException extends ResilientException { + private static final String CODE = "E001"; + + public InventoryImportVariableClassNotFoundException(String sheetName, int row, String variableCode, String classCode) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Variable Code [" + variableCode + "]; The class code [" + classCode + "] wasn't found."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(classCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableInvalidValueForUnitTypeException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableInvalidValueForUnitTypeException.java new file mode 100644 index 0000000..eb8a0ad --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableInvalidValueForUnitTypeException.java @@ -0,0 +1,21 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableInvalidValueForUnitTypeException extends ResilientException { + private static final String CODE = "E003"; + + public InventoryImportVariableInvalidValueForUnitTypeException(String sheetName, int row, String variableCode, String value, String valueType) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Variable Code [" + variableCode + "]; The value [" + value + "] it's invalid for type [" + valueType + "]."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(value); + addMessageArg(valueType); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotAllowedForDatafileException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotAllowedForDatafileException.java new file mode 100644 index 0000000..d427e40 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotAllowedForDatafileException.java @@ -0,0 +1,16 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableNotAllowedForDatafileException extends ResilientException { + private static final String CODE = "E022"; + + public InventoryImportVariableNotAllowedForDatafileException(String variableCode) { + super("Variable Code [" + variableCode + "]; This variable is not configured to allow insertion from a datafile."); + addMessageArg(variableCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotAllowedForInputException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotAllowedForInputException.java new file mode 100644 index 0000000..50b3973 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotAllowedForInputException.java @@ -0,0 +1,16 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableNotAllowedForInputException extends ResilientException { + private static final String CODE = "E021"; + + public InventoryImportVariableNotAllowedForInputException(String variableCode) { + super("Variable Code [" + variableCode + "]; This variable is not configured from input."); + addMessageArg(variableCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotFoundException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotFoundException.java new file mode 100644 index 0000000..e4ef513 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableNotFoundException.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableNotFoundException extends ResilientException { + private static final String CODE = "E004"; + + public InventoryImportVariableNotFoundException(String sheetName, int row, String variableCode) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; The variable code [" + variableCode + "] wasn't found."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitConverterFormulaException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitConverterFormulaException.java new file mode 100644 index 0000000..c1de888 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitConverterFormulaException.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableUnitConverterFormulaException extends ResilientException { + private static final String CODE = "E010"; + + public InventoryImportVariableUnitConverterFormulaException(String sheetName, int row, String variableCode, + String fromUnitCode, String toUnitCode, String formula) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Variable Code [" + variableCode + + "]; Unit convertion formula exception to convert the value from the unit [" + fromUnitCode + "] in the unit [" + + toUnitCode + "], with formula [" + formula + "]."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(fromUnitCode); + addMessageArg(toUnitCode); + addMessageArg(formula); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} \ No newline at end of file diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitConverterNotFoundException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitConverterNotFoundException.java new file mode 100644 index 0000000..9e942ed --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitConverterNotFoundException.java @@ -0,0 +1,21 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableUnitConverterNotFoundException extends ResilientException { + private static final String CODE = "E006"; + + public InventoryImportVariableUnitConverterNotFoundException(String sheetName, int row, String variableCode, String fromUnitCode, String toUnitCode) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Variable Code [" + variableCode + "]; A unit convertion is needed, but no converter is configured to convert the value from the unit [" + fromUnitCode + "] in the unit [" + toUnitCode + "]."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(fromUnitCode); + addMessageArg(toUnitCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitNotAllowedException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitNotAllowedException.java new file mode 100644 index 0000000..c9b130d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitNotAllowedException.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableUnitNotAllowedException extends ResilientException { + private static final String CODE = "E002"; + + public InventoryImportVariableUnitNotAllowedException(String sheetName, int row, String variableCode, String unitCode) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Variable Code [" + variableCode + "]; The unit code [" + unitCode + "] not allowed in the variable."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(unitCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitNotFoundException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitNotFoundException.java new file mode 100644 index 0000000..384e920 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportVariableUnitNotFoundException.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportVariableUnitNotFoundException extends ResilientException { + private static final String CODE = "E005"; + + public InventoryImportVariableUnitNotFoundException(String sheetName, int row, String variableCode, String unitCode) { + super("Sheet [" + sheetName + "]; Row[" + row + "]; Variable Code [" + variableCode + "]; The unit code [" + unitCode + "] wasn't found."); + + addMessageArg(sheetName); + addMessageArg(row); + addMessageArg(variableCode); + addMessageArg(unitCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/InventoryImportYearOfDataDontMatchRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/InventoryImportYearOfDataDontMatchRuntimeException.java new file mode 100644 index 0000000..9209039 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/InventoryImportYearOfDataDontMatchRuntimeException.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.error; + +public class InventoryImportYearOfDataDontMatchRuntimeException extends ResilientRuntimeException { + private static final String CODE = "E025"; + + public InventoryImportYearOfDataDontMatchRuntimeException(Integer periodYear, Integer fileYear) { + super("The InputDataUpload configured period year is NOT the same year within the file. The period year is [" + + periodYear + "] and the file year is [" + fileYear + "]."); + + addMessageArg(periodYear); + addMessageArg(fileYear); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/OutputExpressionEvaluationRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/OutputExpressionEvaluationRuntimeException.java new file mode 100644 index 0000000..3dc2b51 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/OutputExpressionEvaluationRuntimeException.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.error; + +public class OutputExpressionEvaluationRuntimeException extends ResilientRuntimeException { + private static final String CODE = "E009"; + + public OutputExpressionEvaluationRuntimeException(String outputVariableCode, String organizationCode, String period, + String periodVersion, String expression, ExpressionEvaluationException cause) { + super("An error occurred calculating the output: Variable Code=" + outputVariableCode + ", Organization Code=" + + organizationCode + ", Period=" + period + ", Period Version=" + periodVersion + ", Expression=" + expression + + ". With the error: " + cause.getMessage(), cause); + + addMessageArg(outputVariableCode); + addMessageArg(organizationCode); + addMessageArg(period); + addMessageArg(periodVersion); + addMessageArg(expression); + addMessageArg(cause.getMessage()); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/OutputExpressionNullResultRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/OutputExpressionNullResultRuntimeException.java new file mode 100644 index 0000000..f87e9a0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/OutputExpressionNullResultRuntimeException.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.error; + +public class OutputExpressionNullResultRuntimeException extends ResilientRuntimeException { + private static final String CODE = "E016"; + + public OutputExpressionNullResultRuntimeException(String outputVariableCode, String organizationCode, String period, + String periodVersion, String expression) { + super("The expression evaluation resulted in null value: Variable Code [" + outputVariableCode + + "], Organization Code [" + organizationCode + "], Period [" + period + "], Period Version [" + periodVersion + + "], Expression [" + expression + "]. "); + + addMessageArg(outputVariableCode); + addMessageArg(organizationCode); + addMessageArg(period); + addMessageArg(periodVersion); + addMessageArg(expression); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/PeriodProcessDependenciesNotSatisfyedException.java b/src/main/java/com/oguerreiro/resilient/error/PeriodProcessDependenciesNotSatisfyedException.java new file mode 100644 index 0000000..feb2803 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/PeriodProcessDependenciesNotSatisfyedException.java @@ -0,0 +1,58 @@ +package com.oguerreiro.resilient.error; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.oguerreiro.resilient.domain.Variable; + +public class PeriodProcessDependenciesNotSatisfyedException extends ResilientRuntimeException { + private static final String CODE = "E012"; + + private Map> remainingVariableDependencies; + + public PeriodProcessDependenciesNotSatisfyedException(Map> remainingVariableDependencies) { + super("Dependencies not satisfyed"); + + this.remainingVariableDependencies = remainingVariableDependencies; + + addMessageArg(this.buildDependencyArg()); + } + + @Override + public String getMessageCode() { + return CODE; + } + + public void printDependencies() { + for (Entry> entry : remainingVariableDependencies.entrySet()) { + System.out.print(entry.getKey().getCode() + " depends of "); + for (String dependency : entry.getValue()) { + System.out.print(dependency + " ; "); + } + System.out.println(); + } + } + + private String buildDependencyArg() { + StringBuilder arg = new StringBuilder(); + + boolean first = true; + int max = 10; + for (Entry> entry : remainingVariableDependencies.entrySet()) { + if (!first) + arg.append(";"); + arg.append(entry.getKey().getCode()); + + first = false; + max--; + + if (max == 0) { + arg.append("; ..."); + break; + } + } + + return arg.toString(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/error/ResilientException.java b/src/main/java/com/oguerreiro/resilient/error/ResilientException.java new file mode 100644 index 0000000..20b871b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ResilientException.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.error; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ResilientException extends Exception { + private List messageArgs; + + public ResilientException(String message) { + super(message); + } + + public abstract String getMessageCode(); + + protected void addMessageArg(Object arg) { + if (this.messageArgs == null) + this.messageArgs = new ArrayList(); + + this.messageArgs.add(arg); + } + + protected List args() { + return this.messageArgs; + } + + public Object[] getMessageArgs() { + return this.messageArgs.toArray(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/error/ResilientJpaMethodNotImplementedRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/ResilientJpaMethodNotImplementedRuntimeException.java new file mode 100644 index 0000000..7a39184 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ResilientJpaMethodNotImplementedRuntimeException.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.error; + +import com.oguerreiro.resilient.repository.ResilientJpaRepository; + +/** + * Exception used in {@link ResilientJpaRepository} interface, for conceptual(default) methods that specific + * implementation + * should (re)define. + */ +public class ResilientJpaMethodNotImplementedRuntimeException extends ResilientRuntimeException { + private static final long serialVersionUID = 1L; + private static final String CODE = "E017"; + + public ResilientJpaMethodNotImplementedRuntimeException(String jpaRepositoryClassName, + String jpaRepositoryMethodName) { + super( + "Resilient JPA method not implemented. Entity JPA interfaces must provide custom eager loading methods. See documentation for " + + jpaRepositoryClassName + "." + jpaRepositoryMethodName + "."); + + addMessageArg(jpaRepositoryClassName); + addMessageArg(jpaRepositoryMethodName); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/ResilientRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/ResilientRuntimeException.java new file mode 100644 index 0000000..017040c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/ResilientRuntimeException.java @@ -0,0 +1,34 @@ +package com.oguerreiro.resilient.error; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ResilientRuntimeException extends RuntimeException { + private static final long serialVersionUID = 1L; + private List messageArgs; + + public ResilientRuntimeException(String message) { + super(message); + } + + public ResilientRuntimeException(String message, Throwable cause) { + super(message, cause); + } + + public abstract String getMessageCode(); + + protected void addMessageArg(Object arg) { + if (this.messageArgs == null) + this.messageArgs = new ArrayList(); + + this.messageArgs.add(arg); + } + + protected List args() { + return this.messageArgs; + } + + public Object[] getMessageArgs() { + return this.messageArgs.toArray(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/error/UnitConverterExpressionEvaluationRuntimeException.java b/src/main/java/com/oguerreiro/resilient/error/UnitConverterExpressionEvaluationRuntimeException.java new file mode 100644 index 0000000..6e92d28 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/UnitConverterExpressionEvaluationRuntimeException.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.error; + +public class UnitConverterExpressionEvaluationRuntimeException extends ResilientRuntimeException { + private static final String CODE = "E013"; + + public UnitConverterExpressionEvaluationRuntimeException(String variableCode, String fromUnitCode, String toUnitCode, + String sourceValue, ExpressionEvaluationException cause) { + super("An error occurred calculating the unit converter: Variable Code [" + variableCode + "], From Unit Code [" + + fromUnitCode + "], To Unit Code [" + toUnitCode + "], Value [" + sourceValue + "]. With the following error: " + + cause.getMessage()); + + addMessageArg(variableCode); + addMessageArg(fromUnitCode); + addMessageArg(toUnitCode); + addMessageArg(sourceValue); + addMessageArg(cause.getMessage()); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterMoreThanOneFoundException.java b/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterMoreThanOneFoundException.java new file mode 100644 index 0000000..1f9befc --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterMoreThanOneFoundException.java @@ -0,0 +1,40 @@ +package com.oguerreiro.resilient.error; + +public class VariableUnitConverterMoreThanOneFoundException extends ResilientException { + private static final String CODE = "E020"; + + private final String variableCode; + private final String fromUnitCode; + private final String toUnitCode; + + public VariableUnitConverterMoreThanOneFoundException(String variableCode, String fromUnitCode, String toUnitCode) { + super("Variable Code [" + variableCode + "]; The unit converter from unit [" + fromUnitCode + "] to unit [" + + toUnitCode + "] found multiple converters. Only one is expected. This is a configuration error."); + + this.variableCode = variableCode; + this.fromUnitCode = fromUnitCode; + this.toUnitCode = toUnitCode; + + addMessageArg(variableCode); + addMessageArg(fromUnitCode); + addMessageArg(toUnitCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + + public String getVariableCode() { + return variableCode; + } + + public String getFromUnitCode() { + return fromUnitCode; + } + + public String getToUnitCode() { + return toUnitCode; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterNotFoundException.java b/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterNotFoundException.java new file mode 100644 index 0000000..d2cd657 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterNotFoundException.java @@ -0,0 +1,41 @@ +package com.oguerreiro.resilient.error; + +public class VariableUnitConverterNotFoundException extends ResilientException { + private static final String CODE = "E018"; + + private final String variableCode; + private final String fromUnitCode; + private final String toUnitCode; + + public VariableUnitConverterNotFoundException(String variableCode, String fromUnitCode, String toUnitCode) { + super("Variable Code [" + variableCode + + "]; A unit convertion is needed, but no converter is configured to convert the value from the unit [" + + fromUnitCode + "] in the unit [" + toUnitCode + "]."); + + this.variableCode = variableCode; + this.fromUnitCode = fromUnitCode; + this.toUnitCode = toUnitCode; + + addMessageArg(variableCode); + addMessageArg(fromUnitCode); + addMessageArg(toUnitCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + + public String getVariableCode() { + return variableCode; + } + + public String getFromUnitCode() { + return fromUnitCode; + } + + public String getToUnitCode() { + return toUnitCode; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterWithoutFormulaException.java b/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterWithoutFormulaException.java new file mode 100644 index 0000000..709e782 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/error/VariableUnitConverterWithoutFormulaException.java @@ -0,0 +1,40 @@ +package com.oguerreiro.resilient.error; + +public class VariableUnitConverterWithoutFormulaException extends ResilientException { + private static final String CODE = "E019"; + + private final String variableCode; + private final String fromUnitCode; + private final String toUnitCode; + + public VariableUnitConverterWithoutFormulaException(String variableCode, String fromUnitCode, String toUnitCode) { + super("Variable Code [" + variableCode + "]; The unit converter from unit [" + fromUnitCode + "] to unit [" + + toUnitCode + "] exists, but doesn't have a convertion formula defined. This is a configuration error."); + + this.variableCode = variableCode; + this.fromUnitCode = fromUnitCode; + this.toUnitCode = toUnitCode; + + addMessageArg(variableCode); + addMessageArg(fromUnitCode); + addMessageArg(toUnitCode); + } + + @Override + public String getMessageCode() { + return CODE; + } + + public String getVariableCode() { + return variableCode; + } + + public String getFromUnitCode() { + return fromUnitCode; + } + + public String getToUnitCode() { + return toUnitCode; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/BaseExpressionFunctions.java b/src/main/java/com/oguerreiro/resilient/expression/BaseExpressionFunctions.java new file mode 100644 index 0000000..77fea0e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/BaseExpressionFunctions.java @@ -0,0 +1,79 @@ +package com.oguerreiro.resilient.expression; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * Base functions + */ +public class BaseExpressionFunctions implements ExpressionFunctions { + + private ThreadLocal traceEnabled = ThreadLocal.withInitial(() -> Boolean.FALSE); + private ThreadLocal> trace; + + public void error(String message) { + throw new RuntimeException(message); + } + + protected BigDecimal safeBigDecimal(BigDecimal value) { + if (value == null) { + return BigDecimal.ZERO; + } + return value; + } + + public final Boolean isTraceEnabled() { + return this.traceEnabled.get(); + } + + public final void enableTrace() { + this.traceEnabled.set(Boolean.TRUE); + this.trace.set(new ArrayList()); + } + + public final List stopTrace() { + this.traceEnabled.set(Boolean.FALSE); + List myTrace = this.trace.get(); + this.trace.set(null); + + return myTrace; + } + + public final void addTrace(String function, String warn, Object result, Object... args) { + if (this.isTraceEnabled()) { + StringBuilder functionCall = new StringBuilder(); + + // Function call signature: in('010101', 'xpto') + functionCall.append(function).append("("); + boolean isFirst = true; + for (Object object : args) { + if (!isFirst) + functionCall.append(" , "); + + functionCall.append(traceValue(object)); + + isFirst = false; + } + functionCall.append(")"); + + // Function result: = 0,9876 + functionCall.append("=").append(traceValue(result)); + + // Function warning: *WARN: warning text + if (warn != null && !warn.isBlank()) { + functionCall.append(" *WARN: ").append(warn); + } + + this.trace.get().add(functionCall.toString()); + } + } + + private String traceValue(Object value) { + if (value instanceof String) { + return "'" + value.toString() + "'"; + } else { + return value.toString(); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/EmissionFactorExpressionFunctions.java b/src/main/java/com/oguerreiro/resilient/expression/EmissionFactorExpressionFunctions.java new file mode 100644 index 0000000..ee9a91a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/EmissionFactorExpressionFunctions.java @@ -0,0 +1,43 @@ +package com.oguerreiro.resilient.expression; + +import java.math.BigDecimal; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.oguerreiro.resilient.domain.EmissionFactor; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.expression.error.EmissionFactorNotFoundExpressionFunctionException; +import com.oguerreiro.resilient.expression.error.ResilientExpressionFunctionException; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; + +/** + * Base math functions + */ +public class EmissionFactorExpressionFunctions extends MathExpressionFunctions { + + @Autowired + private EmissionFactorRepository emissionFactorRepository; + private Integer currentPeriodYear; + + public EmissionFactorExpressionFunctions(Period currentPeriod) { + this.currentPeriodYear = currentPeriod.getBeginDate().getYear(); + } + + public BigDecimal fe(String emissionFactorCode) throws ResilientExpressionFunctionException { + // Automatically add the current Period Year. + return fe(emissionFactorCode, this.currentPeriodYear); + } + + public BigDecimal fe(String emissionFactorCode, Integer year) throws ResilientExpressionFunctionException { + List emissionFactors = this.emissionFactorRepository.findByCodeAndYearOrderByYear( + emissionFactorCode, year); + + if (emissionFactors == null || emissionFactors.isEmpty()) { + throw new EmissionFactorNotFoundExpressionFunctionException(emissionFactorCode, year); + } + + return emissionFactors.get(0).getValue(); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/ExpressionContext.java b/src/main/java/com/oguerreiro/resilient/expression/ExpressionContext.java new file mode 100644 index 0000000..54ba2da --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/ExpressionContext.java @@ -0,0 +1,52 @@ +package com.oguerreiro.resilient.expression; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +public class ExpressionContext { + private Map classInputs = new HashMap<>(); + private Map valueInputs = new HashMap<>(); + + private Map classImports = new HashMap<>(); + private Map methodImports = new HashMap<>(); + + private ExpressionFunctions expressionFunctions; + + public void setExpressionFunctions(ExpressionFunctions expressionFunctions) { + this.expressionFunctions = expressionFunctions; + } + + public ExpressionFunctions getExpressionFunctions() { + return this.expressionFunctions; + } + + public void addInput(String name, Class clazz, Object value) { + this.classInputs.put(name, clazz); + this.valueInputs.put(name, value); + } + + public void addImport(String name, Class clazz) { + this.classImports.put(name, clazz); + } + + public void addImport(String name, Method method) { + this.methodImports.put(name, method); + } + + public Map getClassInputs() { + return this.classInputs; + } + + public Map getValueInputs() { + return this.valueInputs; + } + + public Map getClassImports() { + return this.classImports; + } + + public Map getMethodImports() { + return this.methodImports; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/ExpressionFunctions.java b/src/main/java/com/oguerreiro/resilient/expression/ExpressionFunctions.java new file mode 100644 index 0000000..10fc7d3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/ExpressionFunctions.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.expression; + +/** + * ExpressionFunctions interface to identify functions provider for the ExpressionContext + */ +public interface ExpressionFunctions { + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/ExpressionUtils.java b/src/main/java/com/oguerreiro/resilient/expression/ExpressionUtils.java new file mode 100644 index 0000000..19d0ec2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/ExpressionUtils.java @@ -0,0 +1,175 @@ +package com.oguerreiro.resilient.expression; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.mvel2.CompileException; +import org.mvel2.MVEL; +import org.mvel2.ParserContext; + +import com.oguerreiro.resilient.error.ExpressionEvaluationException; +import com.oguerreiro.resilient.expression.error.ResilientExpressionFunctionException; + +public class ExpressionUtils { + public static Serializable compileExpression(String expression, ExpressionContext context) { + try { + // Create a ParserContext + ParserContext pContext = new ParserContext(); + pContext.addInputs(context.getClassInputs()); + for (Entry importClass : context.getClassImports().entrySet()) { + pContext.addImport(importClass.getKey(), importClass.getValue()); + } + for (Entry importMethod : context.getMethodImports().entrySet()) { + pContext.addImport(importMethod.getKey(), importMethod.getValue()); + } + + // Compile expression + Serializable compiledExpression = MVEL.compileExpression(expression, pContext); // Check syntax + return compiledExpression; + } catch (CompileException e) { + throw e; + } + } + + public static Object executeExpression(Serializable compiledExpression, ExpressionFunctions expressionFunctions, + Map vars) { + // Evaluate the compiled expression with the context + Object result = null; + if (expressionFunctions != null) { + result = MVEL.executeExpression(compiledExpression, expressionFunctions, vars); + } else { + result = MVEL.executeExpression(compiledExpression, vars); + } + + return result; + } + + public static Object evalExpression(String expression, ExpressionContext context) + throws ExpressionEvaluationException { + try { + Serializable compiledExpression = compileExpression(expression, context); + Object result = executeExpression(compiledExpression, context.getExpressionFunctions(), context.getValueInputs()); + return result; + } catch (Throwable t) { + ResilientExpressionFunctionException functionException = null; + + //Catch any error. And then, evaluate the cause. + if ((t instanceof ArithmeticException) && t.getMessage().contains("Division undefined")) { + // This is a HACK to overcome problems with output formula configuration. + // If a divide by ZERO is encoutered, just return ZERO to prevent errors. + System.out.println("************************************************************************"); + System.out.println("CRITICAL FORMULA ERROR WITH EXPRESSION : " + expression); + System.out.println("************************************************************************"); + + return BigDecimal.ZERO; + } else if (!(t instanceof ResilientExpressionFunctionException)) { + // The MVEL will obfuscate the original exception, must drill-down to target + Throwable cause = t.getCause(); + if (cause != null) { + if (cause instanceof ResilientExpressionFunctionException) { + functionException = (ResilientExpressionFunctionException) cause; + } else if (cause instanceof InvocationTargetException) { + InvocationTargetException invocationTargetException = (InvocationTargetException) cause; + Throwable targetException = invocationTargetException.getTargetException(); + + if (targetException != null && targetException instanceof ResilientExpressionFunctionException) { + functionException = (ResilientExpressionFunctionException) targetException; + } + } + } + } else { + functionException = (ResilientExpressionFunctionException) t; + } + + if (functionException != null) { + // This is a managed Resilient exception throwed by a custom function. + throw new ExpressionEvaluationException(expression, functionException); + } else { + // Rethrow original exception + throw new ExpressionEvaluationException(expression, t); + } + } + + } + + /** + * Validations are limited. Validates syntax. And identifies the usage of Inputs, not provided by the context. + * It DOESN'T checks the usage of invalid methods in classes (imports). This will be done in runtime only. + * + * @param expression + * @param expressionContext + */ + public static void validateExpression(String expression, ExpressionContext expressionContext) { + try { + // Create a ParserContext for analysis + ParserContext analysisContext = new ParserContext(); + // Add inputs + analysisContext.addInputs(expressionContext.getClassInputs()); + // Add imports + for (Entry importClass : expressionContext.getClassImports().entrySet()) { + analysisContext.addImport(importClass.getKey(), importClass.getValue()); + } + + /* + * Invoke MVEL.analysisCompile with an empty pContext. The analysisCompile method will : + * - Check syntax + * - Fill pContext with inputs[] and variables[] + * With this strategy, I now can compare the analysisContext with the given expressionContext.getClassInputs(), that contains + * the inputs available for this expression. + */ + MVEL.analysisCompile(expression, analysisContext); + + /* + * Compare the used inputs with the inputs available in the give expressionContext + */ + Set availableInputsKeys = expressionContext.getClassInputs().keySet(); + Set analysisInputsKeys = analysisContext.getInputs().keySet(); // Inputs the expression actually uses + + // Find used inputs that are NOT available in expresisonContext + Set missingInputs = new HashSet<>(analysisInputsKeys); + missingInputs.removeAll(availableInputsKeys); + + if (!missingInputs.isEmpty()) { + throw new RuntimeException("Missing inputs in the context: " + missingInputs); + } + } catch (CompileException e) { + throw new RuntimeException("Expression is invalid: " + e.getMessage()); + } catch (SecurityException e) { + throw new RuntimeException("Security failure in expression evaluation: " + e.getMessage()); + } + } + + public static BigDecimal toBigDecimal(Object value, Integer scale) { + BigDecimal bigDecimalValue = toBigDecimal(value); + + if (bigDecimalValue == null || scale == null) + return bigDecimalValue; + + return bigDecimalValue.setScale(scale, RoundingMode.HALF_EVEN); + } + + public static BigDecimal toBigDecimal(Object value) { + if (value == null) + return null; + + if (value.getClass().equals(BigDecimal.class)) { + return (BigDecimal) value; + } else if (value.getClass().equals(Integer.class)) { + return new BigDecimal((Integer) value); + } else if (value.getClass().equals(Long.class)) { + return new BigDecimal((Long) value); + } else if (value.getClass().equals(Double.class)) { + return new BigDecimal((Double) value); + } else { + throw new IllegalArgumentException("Unexpected value: " + value.getClass().getTypeName()); + } + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/FunctionCall.java b/src/main/java/com/oguerreiro/resilient/expression/FunctionCall.java new file mode 100644 index 0000000..fd282d0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/FunctionCall.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.expression; + +import java.util.List; + +public class FunctionCall { + private String name; + private List arguments; + + public FunctionCall(String name, List arguments) { + super(); + this.name = name; + this.arguments = arguments; + } + + public String getName() { + return name; + } + + public List getArguments() { + return arguments; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/MathExpressionFunctions.java b/src/main/java/com/oguerreiro/resilient/expression/MathExpressionFunctions.java new file mode 100644 index 0000000..57662fe --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/MathExpressionFunctions.java @@ -0,0 +1,21 @@ +package com.oguerreiro.resilient.expression; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * Base math functions + */ +public class MathExpressionFunctions extends BaseExpressionFunctions { + + public BigDecimal round(BigDecimal value, Integer precision) { + if (value == null) { + return null; + } + + BigDecimal rounded = value.setScale(3, RoundingMode.HALF_UP); + + return rounded; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/OutputVariableExpressionContext.java b/src/main/java/com/oguerreiro/resilient/expression/OutputVariableExpressionContext.java new file mode 100644 index 0000000..26674a5 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/OutputVariableExpressionContext.java @@ -0,0 +1,81 @@ +package com.oguerreiro.resilient.expression; + +import java.math.BigDecimal; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; + +public class OutputVariableExpressionContext extends ExpressionContext { + private static final String INPUT_VARIABLE = "variable"; // Output variable + private static final String INPUT_PERIOD = "period"; + private static final String INPUT_PERIODVERSION = "periodVersion"; + private static final String INPUT_OWNER = "owner"; //Organization + + private EmissionFactorRepository emissionfactorRepository; + private InputDataRepository inputDataRepository; + + /** + * Sets the inputs names and types. Basic Context for validation + */ + private OutputVariableExpressionContext(EmissionFactorRepository emissionfactorRepository, + InputDataRepository inputDataRepository) { + this(emissionfactorRepository, inputDataRepository, null, null, null, null); + } + + private OutputVariableExpressionContext(EmissionFactorRepository emissionfactorRepository, + InputDataRepository inputDataRepository, Variable variable, Period period, PeriodVersion periodVersion, + Organization owner) { + this.addInput(INPUT_VARIABLE, Variable.class, variable); + this.addInput(INPUT_PERIOD, Period.class, period); + this.addInput(INPUT_PERIODVERSION, PeriodVersion.class, periodVersion); + this.addInput(INPUT_OWNER, Organization.class, owner); + + this.emissionfactorRepository = emissionfactorRepository; + this.inputDataRepository = inputDataRepository; + } + + public static OutputVariableExpressionContext create(EmissionFactorRepository emissionfactorRepository, + InputDataRepository inputDataRepository, Variable variable, Period period, PeriodVersion periodVersion, + Organization owner) { + return new OutputVariableExpressionContext(emissionfactorRepository, inputDataRepository, variable, period, + periodVersion, owner); + } + + public static OutputVariableExpressionContext create(EmissionFactorRepository emissionfactorRepository, + InputDataRepository inputDataRepository) { + return new OutputVariableExpressionContext(emissionfactorRepository, inputDataRepository); + } + + public static BigDecimal in(Object... codes) { + // codes[0] - (String) VariableCode + // codes[1] - (String) ClassCode + + if (codes.length == 1) { + return OutputVariableExpressionContext.in((String) codes[0]); + } else if (codes.length == 2) { + return OutputVariableExpressionContext.in((String) codes[0], (String) codes[1]); + } + + return null; + } + + private static BigDecimal in(String variableCode) { + return null; + } + + private static BigDecimal in(String variableCode, String classCode) { + return null; + } + + public static BigDecimal out(String variableCode) { + return null; + } + + public static BigDecimal fe(String emissionFactorCode) { + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/OutputVariableExpressionFunctions.java b/src/main/java/com/oguerreiro/resilient/expression/OutputVariableExpressionFunctions.java new file mode 100644 index 0000000..4f841c8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/OutputVariableExpressionFunctions.java @@ -0,0 +1,158 @@ +package com.oguerreiro.resilient.expression; + +import java.math.BigDecimal; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.OutputDataRepository; +import com.oguerreiro.resilient.service.OutputDataService; + +/** + * This s a Expressionfunction's provider. To simplify development, an instance must be created an then call + * autowireBean() to inject other beans + */ +public class OutputVariableExpressionFunctions extends EmissionFactorExpressionFunctions { + + @Autowired + private InputDataRepository inputDataRepository; + @Autowired + private OutputDataRepository outputDataRepository; + @Autowired + private OutputDataService outputDataService; + @Autowired + private OrganizationRepository organizationRepository; + + private Period currentPeriod; + private PeriodVersion currentPeriodVersion; + private Organization currentOrganization; + + public OutputVariableExpressionFunctions(Period currentPeriod, PeriodVersion currentPeriodVersion, + Organization currentOrganization) { + super(currentPeriod); + this.currentOrganization = currentOrganization; + this.currentPeriod = currentPeriod; + this.currentPeriodVersion = currentPeriodVersion; + } + + /* Public accessible functions in expressions */ + public BigDecimal out(String variableCode) { + //Wildcard prepare + variableCode = variableCode.replace('*', '%'); + + BigDecimal value = this.outputDataService.sumAllOutputsFor(this.currentOrganization, this.currentPeriodVersion, + variableCode); + BigDecimal result = safeBigDecimal(value); + + // addTrace (function, warn, result, args...) + addTrace("out", "", result, variableCode, this.currentOrganization.getCode()); + + return result; + } + + public BigDecimal out(String variableCode, String ownerCode) { + BigDecimal value = BigDecimal.ZERO; + + //Wildcard prepare + variableCode = variableCode.replace('*', '%'); + + //Search Organization for the given ownerCode + Organization organization = organizationRepository.findOneByCode(ownerCode).orElse(null); + String warn = ""; + if (organization != null) { + value = this.outputDataService.sumAllOutputsFor(organization, this.currentPeriodVersion, variableCode); + } else { + warn = "Organization not found for code [" + ownerCode + "]"; + } + BigDecimal result = safeBigDecimal(value); + + // addTrace (function, warn, result, args...) + addTrace("out", warn, result, variableCode, ownerCode); + + return result; + } + + public BigDecimal outByUnit(String unitCode) { + BigDecimal value = this.outputDataRepository.sumAllOutputsForUnit(this.currentOrganization, + this.currentPeriodVersion, unitCode); + BigDecimal result = safeBigDecimal(value); + + // addTrace (function, warn, result, args...) + addTrace("outByUnit", "", result, unitCode, this.currentOrganization.getCode()); + + return result; + } + + public BigDecimal outByUnit(String unitCode, String ownerCode) { + BigDecimal value = BigDecimal.ZERO; + + //Search Organization for the given ownerCode + Organization organization = organizationRepository.findOneByCode(ownerCode).orElse(null); + String warn = ""; + + if (organization != null) { + value = this.outputDataRepository.sumAllOutputsForUnit(organization, this.currentPeriodVersion, unitCode); + } else { + warn = "Organization not found for code [" + ownerCode + "]"; + } + + BigDecimal result = safeBigDecimal(value); + + // addTrace (function, warn, result, args...) + addTrace("outByUnit", warn, result, unitCode, ownerCode); + + return result; + } + + public BigDecimal in(String variableCode) { + return inFunction(variableCode, "*ALL", this.currentOrganization.getCode(), this.currentOrganization); + } + + public BigDecimal in(String variableCode, String classCode) { + return inFunction(variableCode, classCode, this.currentOrganization.getCode(), this.currentOrganization); + } + + public BigDecimal in(String variableCode, String classCode, String ownerCode) { + //Search Organization for the given ownerCode + Organization organization = organizationRepository.findOneByCode(ownerCode).orElse(null); + + return inFunction(variableCode, classCode, ownerCode, organization); + } + + private BigDecimal inFunction(String variableCode, String classCode, String ownerCode, Organization organization) { + //Wildcard prepare + variableCode = variableCode.replace('*', '%'); + + //ClassCode pre-processing. If *ALL, convert to null + if (classCode != null && classCode.toUpperCase().equals("*ALL")) { + classCode = null; + } + + BigDecimal result = BigDecimal.ZERO; + String warn = ""; + if (organization != null) { + if (classCode != null) { + result = this.inputDataRepository.sumAllInputsFor(organization, this.currentPeriod, variableCode, classCode); + } else { + result = this.inputDataRepository.sumAllInputsFor(organization, this.currentPeriod, variableCode); + } + } else { + warn = "Organization not found for code [" + ownerCode + "]"; + } + + // Make sure NEVER to return null + result = safeBigDecimal(result); + + // addTrace (function, warn, result, args...) + addTrace("in", warn, result, variableCode, classCode == null ? "*ALL" : classCode, ownerCode); + + return result; + } + + /* Private methods */ + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/UnitConverterExpressionContext.java b/src/main/java/com/oguerreiro/resilient/expression/UnitConverterExpressionContext.java new file mode 100644 index 0000000..ea5cf6f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/UnitConverterExpressionContext.java @@ -0,0 +1,70 @@ +package com.oguerreiro.resilient.expression; + +import java.math.BigDecimal; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; + +public class UnitConverterExpressionContext extends ExpressionContext { + private static final String INPUT_YEAR = "year"; + private static final String INPUT_VALUE = "value"; + private static final String INPUT_FROM_UNIT = "fromUnit"; + private static final String INPUT_TO_UNIT = "toUnit"; + private static final String INPUT_VARIABLE = "variable"; + private static final String INPUT_CLASS_CODE = "classCode"; + + private static final String FUNCTION_EMISSIONFACTOR = "FEmission"; + + private EmissionFactorRepository emissionfactorRepository; + + /** + * Sets the inputs names and types. Basic Context for validation + */ + private UnitConverterExpressionContext(EmissionFactorRepository emissionfactorRepository) { + this(null, null, null, null, null, null, emissionfactorRepository); + } + + private UnitConverterExpressionContext(Integer year, Object value, Unit fromUnit, Unit toUnit, Variable variable, + String classCode, EmissionFactorRepository emissionfactorRepository) { + this.addInput(INPUT_YEAR, Integer.class, year); + this.addInput(INPUT_VALUE, BigDecimal.class, value); + this.addInput(INPUT_FROM_UNIT, Unit.class, fromUnit); + this.addInput(INPUT_TO_UNIT, Unit.class, toUnit); + this.addInput(INPUT_VARIABLE, Variable.class, variable); + this.addInput(INPUT_CLASS_CODE, String.class, classCode); + + this.addInput(FUNCTION_EMISSIONFACTOR, FunctionEmissionFactor.class, + new FunctionEmissionFactor(year, emissionfactorRepository)); + + this.emissionfactorRepository = emissionfactorRepository; + } + + public static UnitConverterExpressionContext create(EmissionFactorRepository emissionfactorRepository, Integer year, + Object value, Unit fromUnit, Unit toUnit, Variable variable, String classCode) { + return new UnitConverterExpressionContext(year, value, fromUnit, toUnit, variable, classCode, + emissionfactorRepository); + } + + public static UnitConverterExpressionContext create(EmissionFactorRepository emissionfactorRepository) { + return new UnitConverterExpressionContext(emissionfactorRepository); + } + + public class FunctionEmissionFactor { + private EmissionFactorRepository emissionfactorRepository; + private Integer currentYear; + + public FunctionEmissionFactor(Integer currentYear, EmissionFactorRepository emissionfactorRepository) { + this.emissionfactorRepository = emissionfactorRepository; + this.currentYear = currentYear; + } + + public Double factor(Integer year, String code) { + return Double.valueOf(0.20); + } + + public Double factor(String code) { + return this.factor(currentYear, code); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/UnitConverterExpressionFunctions.java b/src/main/java/com/oguerreiro/resilient/expression/UnitConverterExpressionFunctions.java new file mode 100644 index 0000000..687a854 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/UnitConverterExpressionFunctions.java @@ -0,0 +1,15 @@ +package com.oguerreiro.resilient.expression; + +import com.oguerreiro.resilient.domain.Period; + +/** + * This is a Expressionfunction's provider. To simplify development, an instance must be created an then call + * autowireBean() to inject other beans + */ +public class UnitConverterExpressionFunctions extends EmissionFactorExpressionFunctions { + + public UnitConverterExpressionFunctions(Period currentPeriod) { + super(currentPeriod); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/error/EmissionFactorNotFoundExpressionFunctionException.java b/src/main/java/com/oguerreiro/resilient/expression/error/EmissionFactorNotFoundExpressionFunctionException.java new file mode 100644 index 0000000..5e8108f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/error/EmissionFactorNotFoundExpressionFunctionException.java @@ -0,0 +1,18 @@ +package com.oguerreiro.resilient.expression.error; + +public class EmissionFactorNotFoundExpressionFunctionException extends ResilientExpressionFunctionException { + private static final String CODE = "E007"; + + public EmissionFactorNotFoundExpressionFunctionException(String code, Integer year) { + super("Emission Factor not found for: Code=" + code + "; Year=" + year.toString()); + + addMessageArg(code); + addMessageArg(year); + } + + @Override + public String getMessageCode() { + return CODE; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/expression/error/ResilientExpressionFunctionException.java b/src/main/java/com/oguerreiro/resilient/expression/error/ResilientExpressionFunctionException.java new file mode 100644 index 0000000..6474e25 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/expression/error/ResilientExpressionFunctionException.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.expression.error; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ResilientExpressionFunctionException extends Exception { + private List messageArgs; + + protected ResilientExpressionFunctionException(String message) { + super(message); + } + + public abstract String getMessageCode(); + + protected void addMessageArg(Object arg) { + if (this.messageArgs == null) + this.messageArgs = new ArrayList(); + + this.messageArgs.add(arg); + } + + protected List args() { + return this.messageArgs; + } + + public Object[] getMessageArgs() { + return this.messageArgs.toArray(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/package-info.java b/src/main/java/com/oguerreiro/resilient/package-info.java new file mode 100644 index 0000000..d6dd07c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/package-info.java @@ -0,0 +1,4 @@ +/** + * Application root. + */ +package com.oguerreiro.resilient; diff --git a/src/main/java/com/oguerreiro/resilient/repository/AuthorityRepository.java b/src/main/java/com/oguerreiro/resilient/repository/AuthorityRepository.java new file mode 100644 index 0000000..8c6d2ee --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/AuthorityRepository.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.Authority; +import org.springframework.data.jpa.repository.*; +import org.springframework.stereotype.Repository; + +/** + * Spring Data JPA repository for the Authority entity. + */ +@SuppressWarnings("unused") +@Repository +public interface AuthorityRepository extends JpaRepository {} diff --git a/src/main/java/com/oguerreiro/resilient/repository/ContentPageRepository.java b/src/main/java/com/oguerreiro/resilient/repository/ContentPageRepository.java new file mode 100644 index 0000000..58b441b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/ContentPageRepository.java @@ -0,0 +1,15 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.ContentPage; +import com.oguerreiro.resilient.domain.enumeration.ContentPageType; + +/** + * Spring Data JPA repository for the ContentPage entity. + */ +@SuppressWarnings("unused") +@Repository +public interface ContentPageRepository extends ResilientJpaRepository { + ContentPage findBySlug(ContentPageType slug); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/DashboardComponentDetailRepository.java b/src/main/java/com/oguerreiro/resilient/repository/DashboardComponentDetailRepository.java new file mode 100644 index 0000000..1c6cf5b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/DashboardComponentDetailRepository.java @@ -0,0 +1,13 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.DashboardComponentDetail; + +/** + * Spring Data JPA repository for the DashboardComponent entity. + */ +@SuppressWarnings("unused") +@Repository +public interface DashboardComponentDetailRepository extends ResilientJpaRepository { +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/DashboardComponentRepository.java b/src/main/java/com/oguerreiro/resilient/repository/DashboardComponentRepository.java new file mode 100644 index 0000000..147250c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/DashboardComponentRepository.java @@ -0,0 +1,47 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.DashboardComponent; +import com.oguerreiro.resilient.domain.DashboardComponentDetailValue; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView; + +/** + * Spring Data JPA repository for the DashboardComponent entity. + */ +@SuppressWarnings("unused") +@Repository +public interface DashboardComponentRepository extends ResilientJpaRepository { + @Query(""" + SELECT new com.oguerreiro.resilient.domain.DashboardComponentDetailValue(d, tdc, o.owner, o.value) + FROM DashboardComponentDetail d + LEFT JOIN OutputData o ON d.variable=o.variable AND o.periodVersion.id=:periodVersionId + LEFT JOIN FETCH d.targetDashboardComponent tdc + WHERE d.dashboardComponent.id = :dashboardComponentId + AND (:organizationId IS NULL OR o.owner.id = :organizationId) + ORDER BY d.indexOrder, d.name + """) + List loadAllDashboardComponentDetailValues( + @Param("organizationId") Long organizationId, @Param("periodVersionId") Long periodVersionId, + @Param("dashboardComponentId") Long dashboardComponentId); + + @Query(""" + SELECT c + FROM DashboardComponent c + WHERE c.isActive = true + """) + List findAllActive(); + + @Query(""" + SELECT c + FROM DashboardComponent c + WHERE c.isActive = true + AND c.view = :dashboardComponentView + """) + List findAllActiveForView( + @Param("dashboardComponentView") DashboardComponentView dashboardComponentView); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/DashboardRepository.java b/src/main/java/com/oguerreiro/resilient/repository/DashboardRepository.java new file mode 100644 index 0000000..95b6763 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/DashboardRepository.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Dashboard; + +/** + * Spring Data JPA repository for the Dashboard entity. + */ +@SuppressWarnings("unused") +@Repository +public interface DashboardRepository extends ResilientJpaRepository { + + @Query(""" + SELECT d + FROM Dashboard d + WHERE d.isInternal = true + """) + List findAllInternal(); + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/DocumentRepository.java b/src/main/java/com/oguerreiro/resilient/repository/DocumentRepository.java new file mode 100644 index 0000000..bbebb11 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/DocumentRepository.java @@ -0,0 +1,13 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Document; + +/** + * Spring Data JPA repository for the Document entity. + */ +@Repository +public interface DocumentRepository extends ResilientJpaRepository { + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/EmissionFactorRepository.java b/src/main/java/com/oguerreiro/resilient/repository/EmissionFactorRepository.java new file mode 100644 index 0000000..7b89b0d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/EmissionFactorRepository.java @@ -0,0 +1,72 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.EmissionFactor; + +/** + * Spring Data JPA repository for the EmissionFactor entity. + */ +@Repository +public interface EmissionFactorRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select emissionFactor from EmissionFactor emissionFactor left join fetch emissionFactor.variableScope left join fetch emissionFactor.variableCategory", countQuery = "select count(emissionFactor) from EmissionFactor emissionFactor") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select emissionFactor from EmissionFactor emissionFactor left join fetch emissionFactor.variableScope left join fetch emissionFactor.variableCategory") + List findAllWithEagerRelationships(); + + @Override + @Query("select emissionFactor from EmissionFactor emissionFactor left join fetch emissionFactor.variableScope left join fetch emissionFactor.variableCategory where emissionFactor.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom EmissionFactorRepository methods */ + /* *************************************** */ + EmissionFactor findByCodeAndYear(String code, Integer year); + + /** + * Intended to get ONLY the first record. This will be the closest EmissionFactor for the provided year + * + * @param code + * @param year + * @return + */ + @Query("SELECT emissionFactor FROM EmissionFactor emissionFactor WHERE emissionFactor.code = :code AND emissionFactor.year<=:year ORDER BY emissionFactor.year DESC") + List findByCodeAndYearOrderByYear(@Param("code") String code, @Param("year") Integer year); + + @Query("SELECT e FROM EmissionFactor e WHERE e.year = :year AND e.variableScope.id=:scopeId AND (:categoryId is null OR e.variableCategory.id=:categoryId) ORDER BY e.year, e.variableScope.name, e.variableCategory.name, e.name") + Stream streamAll(@Param("year") Integer year, @Param("scopeId") Long scopeId, + @Param("categoryId") Long categoryId); + + @Query(""" + SELECT ef + FROM EmissionFactor ef + WHERE ef.year = ( + SELECT MAX(e2.year) + FROM EmissionFactor e2 + WHERE e2.code = ef.code + AND e2.year <= :year + AND e2.value <> 0 + AND (:scopeId IS NULL OR e2.variableScope.id = :scopeId) + AND (:categoryId IS NULL OR e2.variableCategory.id = :categoryId) + ) + AND ef.value <> 0 + AND ef.year <= :year + AND (:scopeId IS NULL OR ef.variableScope.id = :scopeId) + AND (:categoryId IS NULL OR ef.variableCategory.id = :categoryId) + ORDER BY ef.code ASC + """) + List findAllBySearch(@Param("year") Integer year, @Param("scopeId") Long scopeId, + @Param("categoryId") Long categoryId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/InputDataRepository.java b/src/main/java/com/oguerreiro/resilient/repository/InputDataRepository.java new file mode 100644 index 0000000..5349055 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/InputDataRepository.java @@ -0,0 +1,151 @@ +package com.oguerreiro.resilient.repository; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.InventoryData; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.enumeration.InputMode; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; +import com.oguerreiro.resilient.domain.enumeration.UploadType; + +/** + * Spring Data JPA repository for the InputData entity. + */ +@Repository +public interface InputDataRepository + extends ResilientJpaRepository, JpaSpecificationExecutor, InputDataRepositoryExtended { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select inputData from InputData inputData left join fetch inputData.variable left join fetch inputData.period left join fetch inputData.sourceUnit left join fetch inputData.unit left join fetch inputData.owner", countQuery = "select count(inputData) from InputData inputData") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select inputData from InputData inputData left join fetch inputData.variable left join fetch inputData.period left join fetch inputData.sourceUnit left join fetch inputData.unit left join fetch inputData.owner") + List findAllWithEagerRelationships(); + + @Override + @Query("select inputData from InputData inputData left join fetch inputData.variable left join fetch inputData.period left join fetch inputData.sourceUnit left join fetch inputData.unit left join fetch inputData.owner where inputData.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom InputDataRepository methods */ + /* ********************************** */ + /** + * Delete all InputData values that meets the provided arguments AND only + * applies to {@link Variable Variable's} of {@link InputMode#DATAFILE}
+ * This is used to clear Inputdata before a new Datafile is imported + * + * @param organization, the organization that this data belongs to + * @param period, the period of the data + */ + @Modifying // Needed to tell hibernate that changes where done to this entity data. Without it, this records probably should still be shown + @Query("delete from InputData id where id.owner=:organization and id.period=:period and id.variable.inputMode=com.oguerreiro.resilient.domain.enumeration.InputMode.DATAFILE") + void deleteAllInputDataFromDatafile(@Param("organization") Organization organization, @Param("period") Period period); + + /** + * Delete all InputData values that meets the provided arguments AND only + * applies to imported InputData of the same uploadType
+ * This is used to clear Inputdata before a new Datafile is imported + * + * @param organization, the organization that this data belongs to + * @param period, the period of the data + * @param uploadType, the uploadType of the related InputDataUpload. This guarantees that only data imported by the + * same UploadType will be deleted + */ + @Modifying // Needed to tell hibernate that changes where done to this entity data. Without it, this records probably should still be shown + @Query("delete from InputData id where id.owner=:organization and id.period=:period and id.sourceInputDataUpload.type=:uploadType") + void deleteAllInputDataFromDatafile(@Param("organization") Organization organization, @Param("period") Period period, + @Param("uploadType") UploadType uploadType); + + /** + * Delete all InputData values that meets the provided arguments
+ * This is used to clear Inputdata associated to a SourceInputDataUpload + * + * @param inputDataUpload, the InputDataUpload that is to be cleared + */ + @Modifying // Needed to tell hibernate that changes where done to this entity data. Without it, this records probably should still be shown + @Query("delete from InputData id where id.sourceInputDataUpload=:inputDataUpload") + void deleteAllInputDataWithSource(@Param("inputDataUpload") InputDataUpload inputDataUpload); + + @Query("SELECT SUM(id.imputedValue) from InputData id where id.owner=:organization and id.period=:period and id.variable.code LIKE :variableCode") + BigDecimal sumAllInputsFor(@Param("organization") Organization organization, @Param("period") Period period, + @Param("variableCode") String variableCode); + + /** + * Sums all InputData that meet the conditions. Pay attention to the variableCode parameter, that allows wildcard + * search.
+ * InputData with value type {@link UnitValueType#STRING} will be ignored in the query + * + * @param organization par + * @param period + * @param variableCode, this argument allows wildcard search, the constraint is a LIKE clause. Ex. '0101%' + * @param variableClassCode + * @return + */ + @Query("SELECT SUM(id.imputedValue) from InputData id where id.unit.unitType.valueType!=com.oguerreiro.resilient.domain.enumeration.UnitValueType.STRING and id.owner=:organization and id.period=:period and id.variable.code LIKE :variableCode and id.variableClassCode=:variableClassCode") + BigDecimal sumAllInputsFor(@Param("organization") Organization organization, @Param("period") Period period, + @Param("variableCode") String variableCode, @Param("variableClassCode") String variableClassCode); + + @Query("SELECT i FROM InputData i WHERE i.period.id = :periodId ORDER BY i.period.name, i.owner.code, i.variable.code") + Stream streamAll(@Param("periodId") Long periodId); + + @Query(""" + SELECT + new com.oguerreiro.resilient.domain.InventoryData( + id.variable, + id.variableClassCode, + id.variableClassName, + id.unit, + id.stringValue, + SUM(id.imputedValue) + ) + FROM + InputData id + LEFT JOIN Organization o ON id.owner = o + LEFT JOIN Variable v ON id.variable = v + WHERE + id.period.id = :periodId + AND (:orgIds IS NULL OR o.id IN (:orgIds)) + AND (:scopeId IS NULL OR v.variableScope.id=:scopeId) + AND (:categoryId IS NULL OR v.variableCategory.id=:categoryId) + AND id.unit.unitType.valueType NOT IN ( + com.oguerreiro.resilient.domain.enumeration.UnitValueType.BOOLEAN + ) + AND (:hiddenForMain=false OR v.hiddenForMain IS NULL OR (:hiddenForMain=true AND v.hiddenForMain=false)) + group by + id.variable, + CASE + WHEN TRIM(id.variableClassCode) = '' THEN 'EMPTY' + WHEN id.variableClassCode IS NULL THEN 'EMPTY' + ELSE id.variableClassCode + END, + CASE + WHEN TRIM(id.variableClassName) = '' THEN 'EMPTY' + WHEN id.variableClassName IS NULL THEN 'EMPTY' + ELSE id.variableClassName + END, + id.unit, + id.stringValue + order by + v.name, + id.variableClassName + """) + Stream streamAllInventory(@Param("periodId") Long periodId, + @Param("orgIds") List organizationIds, @Param("scopeId") Long scopeId, @Param("categoryId") Long categoryId, + @Param("hiddenForMain") Boolean hiddenForMain); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/InputDataRepositoryExtended.java b/src/main/java/com/oguerreiro/resilient/repository/InputDataRepositoryExtended.java new file mode 100644 index 0000000..8adfafe --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/InputDataRepositoryExtended.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.InputData; + + +public interface InputDataRepositoryExtended { + void saveInNewTransaction(InputData inputData); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadLogRepository.java b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadLogRepository.java new file mode 100644 index 0000000..b484fb0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadLogRepository.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.InputDataUploadLog; + +/** + * Spring Data JPA repository for the InputDataUploadLog entity. + */ +@Repository +public interface InputDataUploadLogRepository + extends ResilientJpaRepository, InputDataUploadLogRepositoryEntended { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select inputDataUploadLog from InputDataUploadLog inputDataUploadLog left join fetch inputDataUploadLog.inputDataUpload", countQuery = "select count(inputDataUploadLog) from InputDataUploadLog inputDataUploadLog") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select inputDataUploadLog from InputDataUploadLog inputDataUploadLog left join fetch inputDataUploadLog.inputDataUpload") + List findAllWithEagerRelationships(); + + @Override + @Query("select inputDataUploadLog from InputDataUploadLog inputDataUploadLog left join fetch inputDataUploadLog.inputDataUpload where inputDataUploadLog.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom InputDataUploadLogRepository methods */ + /* ******************************************* */ + Integer countByInputDataUpload(InputDataUpload inputDataUpload); + + @Query("select inputDataUploadLog from InputDataUploadLog inputDataUploadLog left join fetch inputDataUploadLog.inputDataUpload inputDataUpload where inputDataUpload.id =:id") + List findAllByInputDataUploadId(@Param("id") Long id); + + @Modifying + @Query("delete from InputDataUploadLog l where l.inputDataUpload=:inputDataUpload") + void deleteAllByInputDataUpload(@Param("inputDataUpload") InputDataUpload inputDataUpload); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadLogRepositoryEntended.java b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadLogRepositoryEntended.java new file mode 100644 index 0000000..6e8561e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadLogRepositoryEntended.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.InputDataUploadLog; + + +public interface InputDataUploadLogRepositoryEntended { + void saveInNewTransaction(InputDataUploadLog inputDataUploadLog); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadRepository.java b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadRepository.java new file mode 100644 index 0000000..79209b6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadRepository.java @@ -0,0 +1,34 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.Organization; + +/** + * Spring Data JPA repository for the InputDataUpload entity. + */ +@Repository +public interface InputDataUploadRepository + extends ResilientJpaRepository, InputDataUploadRepositoryEntended { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select inputDataUpload from InputDataUpload inputDataUpload left join fetch inputDataUpload.period left join fetch inputDataUpload.owner", countQuery = "select count(inputDataUpload) from InputDataUpload inputDataUpload") + Page findAllWithEagerRelationships(Pageable pageable); + + @Query("select inputDataUpload from InputDataUpload inputDataUpload left join fetch inputDataUpload.period left join fetch inputDataUpload.owner") + List findAllWithEagerRelationships(); + + @Query("select inputDataUpload from InputDataUpload inputDataUpload left join fetch inputDataUpload.period left join fetch inputDataUpload.owner where inputDataUpload.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + List findByOwnerIn(List oganizations); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadRepositoryEntended.java b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadRepositoryEntended.java new file mode 100644 index 0000000..29d597b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/InputDataUploadRepositoryEntended.java @@ -0,0 +1,8 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.InputDataUpload; + + +public interface InputDataUploadRepositoryEntended { + InputDataUpload updateInNewTransaction(InputDataUpload inputDataUpload); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/MetadataPropertyRepository.java b/src/main/java/com/oguerreiro/resilient/repository/MetadataPropertyRepository.java new file mode 100644 index 0000000..5a06ff6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/MetadataPropertyRepository.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.MetadataProperty; + +/** + * Spring Data JPA repository for the MetadataProperty entity. + */ +@Repository +public interface MetadataPropertyRepository extends ResilientJpaRepository { +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/MetadataValueRepository.java b/src/main/java/com/oguerreiro/resilient/repository/MetadataValueRepository.java new file mode 100644 index 0000000..18f5fc2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/MetadataValueRepository.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.MetadataValue; + +/** + * Spring Data JPA repository for the MetadataValue entity. + */ +@Repository +public interface MetadataValueRepository extends ResilientJpaRepository { +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/OrganizationRepository.java b/src/main/java/com/oguerreiro/resilient/repository/OrganizationRepository.java new file mode 100644 index 0000000..8ba65ab --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/OrganizationRepository.java @@ -0,0 +1,61 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; + +/** + * Spring Data JPA repository for the Organization entity. + */ +@Repository +public interface OrganizationRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select organization from Organization organization left join fetch organization.user left join fetch organization.parent left join fetch organization.organizationType order by (CASE WHEN organization.parent IS NULL THEN 0 ELSE 1 END), (CASE WHEN organization.sort IS NULL THEN 1 ELSE 0 END), organization.sort asc", countQuery = "select count(organization) from Organization organization") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select organization from Organization organization left join fetch organization.user left join fetch organization.parent left join fetch organization.organizationType order by (CASE WHEN organization.parent IS NULL THEN 0 ELSE 1 END), (CASE WHEN organization.sort IS NULL THEN 1 ELSE 0 END), organization.sort asc") + List findAllWithEagerRelationships(); + + @Override + @Query("select organization from Organization organization left join fetch organization.user left join fetch organization.parent left join fetch organization.organizationType where organization.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + @Override + @Query("select organization from Organization organization order by (CASE WHEN organization.parent IS NULL THEN 0 ELSE 1 END), (CASE WHEN organization.sort IS NULL THEN 1 ELSE 0 END), organization.sort asc") + List findAll(); + + /* Custom OrganizationRepository methods */ + /* ************************************* */ + Optional findOneByUser(User user); + + @Query("select organization from Organization organization where organization.outputInventory = true and organization.organizationType.nature = com.oguerreiro.resilient.domain.enumeration.OrganizationNature.ORGANIZATION order by (CASE WHEN organization.parent IS NULL THEN 0 ELSE 1 END), (CASE WHEN organization.sort IS NULL THEN 1 ELSE 0 END), organization.sort asc") + List findAllOrganizationForOutput(); + + Optional findOneByCode(String code); + + @Query(""" + select organization + from Organization organization + where organization.organizationType.nature = :nature + and (:forInput IS NULL or organization.inputInventory = :forInput) + and (:forOutput IS NULL or organization.outputInventory = :forOutput) + order by + (CASE WHEN organization.parent IS NULL THEN 0 ELSE 1 END) + , (CASE WHEN organization.sort IS NULL THEN 1 ELSE 0 END) + , organization.sort asc + """) + List findAllOrganizationByNature(@Param("nature") OrganizationNature nature, + @Param("forInput") Boolean forInput, @Param("forOutput") Boolean forOutput); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeExtendedRepository.java b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeExtendedRepository.java new file mode 100644 index 0000000..13a8696 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeExtendedRepository.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.OrganizationType; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; + +/** + * Spring Data JPA repository for the OrganizationType entity. + * + * When extending this class, extend OrganizationTypeRepositoryWithBagRelationships too. + * For more information refer to https://github.com/jhipster/generator-jhipster/issues/17990. + */ +@Repository +public interface OrganizationTypeExtendedRepository extends OrganizationTypeRepositoryWithBagRelationships, OrganizationTypeRepository { + List findByNatureIn(List natures); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepository.java b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepository.java new file mode 100644 index 0000000..deadf87 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepository.java @@ -0,0 +1,37 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.OrganizationType; + +/** + * Spring Data JPA repository for the OrganizationType entity. + * + * When extending this class, extend OrganizationTypeRepositoryWithBagRelationships too. + * For more information refer to https://github.com/jhipster/generator-jhipster/issues/17990. + */ +@Repository +public interface OrganizationTypeRepository + extends OrganizationTypeRepositoryWithBagRelationships, ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + default Optional findOneWithEagerRelationships(Long id) { + return this.fetchBagRelationships(this.findById(id)); + } + + @Override + default List findAllWithEagerRelationships() { + return this.fetchBagRelationships(this.findAll()); + } + + @Override + default Page findAllWithEagerRelationships(Pageable pageable) { + return this.fetchBagRelationships(this.findAll(pageable)); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepositoryWithBagRelationships.java b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepositoryWithBagRelationships.java new file mode 100644 index 0000000..625b3b2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepositoryWithBagRelationships.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.OrganizationType; +import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Page; + +public interface OrganizationTypeRepositoryWithBagRelationships { + Optional fetchBagRelationships(Optional organizationType); + + List fetchBagRelationships(List organizationTypes); + + Page fetchBagRelationships(Page organizationTypes); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepositoryWithBagRelationshipsImpl.java b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepositoryWithBagRelationshipsImpl.java new file mode 100644 index 0000000..cd3da94 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/OrganizationTypeRepositoryWithBagRelationshipsImpl.java @@ -0,0 +1,67 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.OrganizationType; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +/** + * Utility repository to load bag relationships based on https://vladmihalcea.com/hibernate-multiplebagfetchexception/ + */ +public class OrganizationTypeRepositoryWithBagRelationshipsImpl implements OrganizationTypeRepositoryWithBagRelationships { + + private static final String ID_PARAMETER = "id"; + private static final String ORGANIZATIONTYPES_PARAMETER = "organizationTypes"; + + @PersistenceContext + private EntityManager entityManager; + + @Override + public Optional fetchBagRelationships(Optional organizationType) { + return organizationType.map(this::fetchMetadataProperties); + } + + @Override + public Page fetchBagRelationships(Page organizationTypes) { + return new PageImpl<>( + fetchBagRelationships(organizationTypes.getContent()), + organizationTypes.getPageable(), + organizationTypes.getTotalElements() + ); + } + + @Override + public List fetchBagRelationships(List organizationTypes) { + return Optional.of(organizationTypes).map(this::fetchMetadataProperties).orElse(Collections.emptyList()); + } + + OrganizationType fetchMetadataProperties(OrganizationType result) { + return entityManager + .createQuery( + "select organizationType from OrganizationType organizationType left join fetch organizationType.metadataProperties where organizationType.id = :id", + OrganizationType.class + ) + .setParameter(ID_PARAMETER, result.getId()) + .getSingleResult(); + } + + List fetchMetadataProperties(List organizationTypes) { + HashMap order = new HashMap<>(); + IntStream.range(0, organizationTypes.size()).forEach(index -> order.put(organizationTypes.get(index).getId(), index)); + List result = entityManager + .createQuery( + "select organizationType from OrganizationType organizationType left join fetch organizationType.metadataProperties where organizationType in :organizationTypes", + OrganizationType.class + ) + .setParameter(ORGANIZATIONTYPES_PARAMETER, organizationTypes) + .getResultList(); + Collections.sort(result, (o1, o2) -> Integer.compare(order.get(o1.getId()), order.get(o2.getId()))); + return result; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/OutputDataRepository.java b/src/main/java/com/oguerreiro/resilient/repository/OutputDataRepository.java new file mode 100644 index 0000000..0ea652f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/OutputDataRepository.java @@ -0,0 +1,61 @@ +package com.oguerreiro.resilient.repository; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; + +/** + * Spring Data JPA repository for the OutputData entity. + */ +@Repository +public interface OutputDataRepository extends ResilientJpaRepository, OutputDataRepositoryExtended { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select outputData from OutputData outputData left join fetch outputData.variable left join fetch outputData.period left join fetch outputData.periodVersion left join fetch outputData.baseUnit", countQuery = "select count(outputData) from OutputData outputData") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select outputData from OutputData outputData left join fetch outputData.variable left join fetch outputData.period left join fetch outputData.periodVersion left join fetch outputData.baseUnit") + List findAllWithEagerRelationships(); + + @Override + @Query("select outputData from OutputData outputData left join fetch outputData.variable left join fetch outputData.period left join fetch outputData.periodVersion left join fetch outputData.baseUnit where outputData.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom OutputDataRepository methods */ + /* *********************************** */ + @Modifying + @Query("delete from OutputData o where o.periodVersion = :periodVersion") + void deleteAllByPeriodVersion(@Param("periodVersion") PeriodVersion periodVersion); + + /** + * Output's with value type {@link UnitValueType#STRING} will be ignored in the query + */ + @Query("SELECT SUM(od.value) from OutputData od where od.baseUnit.unitType.valueType!=com.oguerreiro.resilient.domain.enumeration.UnitValueType.STRING and od.owner=:organization and od.periodVersion=:periodVersion and od.variable.code LIKE :variableCode") + BigDecimal sumAllOutputsFor(@Param("organization") Organization organization, + @Param("periodVersion") PeriodVersion periodVersion, @Param("variableCode") String variableCode); + + /** + * Output's with value type {@link UnitValueType#STRING} will be ignored in the query + */ + @Query(value = "SELECT SUM(od.value) from OutputData od where od.baseUnit.unitType.valueType!=com.oguerreiro.resilient.domain.enumeration.UnitValueType.STRING and od.owner=:organization and od.period=:periodVersion and od.variable.baseUnit.code=:unitCode", nativeQuery = true) + BigDecimal sumAllOutputsForUnit(@Param("organization") Organization organization, + @Param("periodVersion") PeriodVersion periodVersion, @Param("unitCode") String unitCode); + + @Query("SELECT o FROM OutputData o WHERE o.periodVersion.id=:periodVersionId ORDER BY o.period.name, o.owner.code, o.variable.code") + Stream streamAll(@Param("periodVersionId") Long periodVersionId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/OutputDataRepositoryExtended.java b/src/main/java/com/oguerreiro/resilient/repository/OutputDataRepositoryExtended.java new file mode 100644 index 0000000..08a6da4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/OutputDataRepositoryExtended.java @@ -0,0 +1,7 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.OutputData; + +public interface OutputDataRepositoryExtended { + void saveInNewTransaction(OutputData outputData); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/PeriodRepository.java b/src/main/java/com/oguerreiro/resilient/repository/PeriodRepository.java new file mode 100644 index 0000000..6d1c18b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/PeriodRepository.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; + +/** + * Spring Data JPA repository for the Period entity. + */ +@Repository +public interface PeriodRepository extends ResilientJpaRepository, JpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select period from Period period left join fetch period.periodVersions", countQuery = "select count(period) from Period period") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select period from Period period left join fetch period.periodVersions") + List findAllWithEagerRelationships(); + + @Override + @Query("select period from Period period left join fetch period.periodVersions where period.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /** + * Find a {@link Period} by its primary key with concurrency check ({@code version}). + * + * @param id + * @param version + * @return + */ + @Query("select period from Period period left join fetch period.periodVersions where period.id =:id and period.version=:version") + Optional findByIdAndVersionWithRelationships(@Param("id") Long id, @Param("version") Integer version); + + List findAllByState(PeriodStatus state); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/PeriodVersionRepository.java b/src/main/java/com/oguerreiro/resilient/repository/PeriodVersionRepository.java new file mode 100644 index 0000000..741ac4b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/PeriodVersionRepository.java @@ -0,0 +1,47 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; + +/** + * Spring Data JPA repository for the PeriodVersion entity. + */ +@Repository +public interface PeriodVersionRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select periodVersion from PeriodVersion periodVersion left join fetch periodVersion.period", countQuery = "select count(periodVersion) from PeriodVersion periodVersion") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select periodVersion from PeriodVersion periodVersion left join fetch periodVersion.period") + List findAllWithEagerRelationships(); + + @Override + @Query("select periodVersion from PeriodVersion periodVersion left join fetch periodVersion.period where periodVersion.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom PeriodVersionRepository methods */ + /* ************************************** */ + @Query("select count(*) from PeriodVersion periodVersion where periodVersion.period =:period and periodVersion.state <> com.oguerreiro.resilient.domain.enumeration.PeriodVersionStatus.ENDED") + long countNotEndedPeriodVersion(@Param("period") Period period); + + @Query("select max(periodVersion.periodVersion) from PeriodVersion periodVersion where periodVersion.period =:period") + long maxPeriodVersion(@Param("period") Period period); + + @Query("select version from PeriodVersion version where version.period.id=:periodId order by version.periodVersion") + List findAllByPeriod(@Param("periodId") Long periodId); + + @Query("select version from PeriodVersion version where version.period.id=:periodId and version.state=com.oguerreiro.resilient.domain.enumeration.PeriodVersionStatus.ENDED ORDER BY version.periodVersion DESC") + List findEndedPeriodVersionsForPeriod(@Param("periodId") Long periodId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/PersistentTokenRepository.java b/src/main/java/com/oguerreiro/resilient/repository/PersistentTokenRepository.java new file mode 100644 index 0000000..ff043c3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/PersistentTokenRepository.java @@ -0,0 +1,16 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.PersistentToken; +import com.oguerreiro.resilient.domain.User; +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Spring Data JPA repository for the {@link PersistentToken} entity. + */ +public interface PersistentTokenRepository extends JpaRepository { + List findByUser(User user); + + List findByTokenDateBefore(LocalDate localDate); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/ResilientJpaRepository.java b/src/main/java/com/oguerreiro/resilient/repository/ResilientJpaRepository.java new file mode 100644 index 0000000..d170a0f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/ResilientJpaRepository.java @@ -0,0 +1,193 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.NoRepositoryBean; + +import com.oguerreiro.resilient.error.ResilientJpaMethodNotImplementedRuntimeException; + +/** + * Resilient base JPA Repository.
+ * All repositories (interfaces) should extend this {@link ResilientJpaRepository}. Simply extend like this: + * + *
+ * @Repository
+ * public interface MySampleRepository extends ResilientJpaRepository {
+ * }
+ * 
+ *

+ * This interface, defines the common functionality of the Resilient framework JpaRepositories. Extending the Jpa + * interfaces: + *

    + *
  • JpaRepository - Common database methods. See {@link ResilientJpaRepository}
  • + *
  • JpaSpecificationExecutor - Provides methods for advanced querying, using {@link Specification}. See + * {@link JpaSpecificationExecutor}
  • + *
+ *

+ * + *

+ * Other repository method's: + *

    + *
  • {@link #findByIdAndVersion(Object, Integer)} - Searches using {@code ID} and {@code Version}. See + * {@link #findByIdAndVersion(Object, Integer)}
  • + *
+ *

+ * + * @param the mapped entity domain class. Example: MySample + * @param the entity domain key class type. Example: Long, assuming the id is a Long + */ +@NoRepositoryBean +public interface ResilientJpaRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * Searches a record by {@code id} and {@code version}. Intended to get read a record from database with a given + * version. This securely guarantees that the intended record is searched, OR, a concurrency colligion has happen. + * + * @param the return type of the repository. Example: MySample [ extends AbstractEntity ] + * @param id the key of the domain + * @param version the record version, to guarantee that the version wasn't changed + * @return + */ + S findByIdAndVersion(ID id, Integer version); + + /* Eager loading */ + /** + * Method used to fetch one domain by its key, with eager relations fetching (OneToOne relations only).
+ * Because JPA is mainly interface methods, this acts like an {@code abstract} method. Subclasses, must redeclare this + * method providing the correct {@code @Query("")} for eager fetching.
+ *

+ * Example: + * + *

+   * 
+   * @Override
+   * @Query("select domain from DomainEntity domain left join fetch domain.oneToOneProperty where domain.id =:id")
+   * Optional<T> findOneWithToOneRelationships(@Param("id") Long id);
+   * 
+   * 
+ * + * In this example, the fetched domain will have the property {@code oneToOneProperty} eagerly initialized. + *

+ * + *

+ * IF the Domain entity have BAG relationships, the override is done to the default it self: + * + *

+   * 
+   * @Override
+   * default Optional<T> findOneWithToOneRelationships(@Param("id") Long id) {
+   *   return this.fetchBagRelationships(this.findById(id));
+   * } 
+   * 
+   * 
+ * + * The {@code fetchBagRelationships} method its a JpaMethod defined in the DomainEntityRepository impl interface. + *

+ * + * @param id the domain id to load + * @return its expected to return a entity domain instance () with all OneToOne relations eagerly + * initialized + * @throws {@link ResilientJpaMethodNotImplementedRuntimeException} if the specific JPA interface for the + * + * doesn't redefine this method. + */ + default Optional findOneWithEagerRelationships(ID id) { + throw new ResilientJpaMethodNotImplementedRuntimeException(this.getClass().getSimpleName(), + "findOneWithEagerRelationships"); + } + + /** + * Method used to fetch all domain's, with eager relations fetching (OneToOne relations only).
+ * Because JPA is mainly interface methods, this acts like an {@code abstract} method. Subclasses, must redeclare this + * method providing the correct {@code @Query("")} for eager fetching.
+ *

+ * Example: + * + *

+   * 
+   * @Override
+   * @Query("select domain from DomainEntity domain left join fetch domain.oneToOneProperty")
+   * List<T> findAllWithEagerRelationships();
+   * 
+   * 
+ * + *

+ * IF the Entity Domain have BAG relationships, the override is done to the default it self: + * + *

+   * 
+   * @Override
+   * default List<T> findAllWithEagerRelationships() {
+   *   return this.fetchBagRelationships(this.findAll());
+   * }
+   * 
+   * 
+ * + * The {@code fetchBagRelationships} method its a JpaMethod defined in the DomainEntityRepository impl interface. + *

+ * + * In this example, the fetched domain collection, will have the property {@code oneToOneProperty} eagerly initialized + * on each domain instance. + *

+ * + * @return its expected to return a list of instance's with all OneToOne relations eagerly initialized + * @throws {@link ResilientJpaMethodNotImplementedRuntimeException} if the specific JPA interface for the + * + * doesn't redefine this method. + */ + default List findAllWithEagerRelationships() { + throw new ResilientJpaMethodNotImplementedRuntimeException(this.getClass().getSimpleName(), + "findAllWithEagerRelationships"); + } + + /** + * Method used to fetch all domain's, with eager relations fetching (OneToOne relations only), with paging.
+ * Because JPA is mainly interface methods, this acts like an {@code abstract} method. Subclasses, must redeclare this + * method providing the correct {@code @Query("")} for eager fetching.
+ *

+ * Example: + * + *

+   * 
+   * @Override
+   * @Query(value = "select domain from DomaimnEntity domain left join fetch domain.oneToOneProperty", countQuery = "select count(domain) from DomainEntity domain")
+   * Page<T> findAllWithEagerRelationships(Pageable pageable);
+   * 
+   * 
+ * + * In this example, the fetched {@link Page}, will have it's collection with the property + * {@code oneToOneProperty} eagerly initialized. + * on each domain instance. + *

+ *

+ * IF the Entity Domain have BAG relationships, the override is done to the default it self: + * + *

+   * 
+   * @Override
+   * default Page<T> findAllWithEagerRelationships(Pageable pageable) {
+   *   return this.fetchBagRelationships(this.findAll(pageable));
+   * }
+   * 
+   * 
+ * + * The {@code fetchBagRelationships} method its a JpaMethod defined in the DomainEntityRepository impl interface. + *

+ * + * @return its expected to return a {@link Page} with it's collection instance's with all OneToOne + * relations eagerly initialized + * @throws {@link ResilientJpaMethodNotImplementedRuntimeException} if the specific JPA interface for the + * + * doesn't redefine this method. + */ + default Page findAllWithEagerRelationships(Pageable pageable) { + throw new ResilientJpaMethodNotImplementedRuntimeException(this.getClass().getSimpleName(), + "findAllWithEagerRelationships"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/ResilientLogRepository.java b/src/main/java/com/oguerreiro/resilient/repository/ResilientLogRepository.java new file mode 100644 index 0000000..8097912 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/ResilientLogRepository.java @@ -0,0 +1,44 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.ResilientLog; + +/** + * Spring Data JPA repository for the InputDataUploadLog entity. + */ +@Repository +public interface ResilientLogRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select resilientLog from ResilientLog resilientLog", countQuery = "select count(resilientLog) from ResilientLog resilientLog") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select resilientLog from ResilientLog resilientLog") + List findAllWithEagerRelationships(); + + @Override + @Query("select resilientLog from ResilientLog resilientLog where resilientLog.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom ResilientLogRepository methods */ + /* ******************************************* */ + Integer countByOwnerId(Long ownerId); + + @Query("select resilientLog from ResilientLog resilientLog where resilientLog.ownerId =:ownerId") + List findAllByOwnerId(@Param("ownerId") Long ownerId); + + @Modifying + @Query("delete from ResilientLog l where l.ownerId=:ownerId") + void deleteAllByOwnerId(@Param("ownerId") Long ownerId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/ResilientNewTransactionRepositoryExtended.java b/src/main/java/com/oguerreiro/resilient/repository/ResilientNewTransactionRepositoryExtended.java new file mode 100644 index 0000000..7f986e6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/ResilientNewTransactionRepositoryExtended.java @@ -0,0 +1,5 @@ +package com.oguerreiro.resilient.repository; + +public interface ResilientNewTransactionRepositoryExtended { + void saveInNewTransaction(T domain); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/ResilientNewTransactionRepositoryExtendedImpl.java b/src/main/java/com/oguerreiro/resilient/repository/ResilientNewTransactionRepositoryExtendedImpl.java new file mode 100644 index 0000000..0d7e821 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/ResilientNewTransactionRepositoryExtendedImpl.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; + +@Repository +public class ResilientNewTransactionRepositoryExtendedImpl implements ResilientNewTransactionRepositoryExtended { + @Autowired + private EntityManager entityManager; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveInNewTransaction(T domain) { + // Persist the entity in a new transaction + entityManager.persist(domain); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/UnitConverterRepository.java b/src/main/java/com/oguerreiro/resilient/repository/UnitConverterRepository.java new file mode 100644 index 0000000..4170d2b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/UnitConverterRepository.java @@ -0,0 +1,37 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.UnitConverter; + +/** + * Spring Data JPA repository for the UnitConverter entity. + */ +@Repository +public interface UnitConverterRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select unitConverter from UnitConverter unitConverter left join fetch unitConverter.fromUnit left join fetch unitConverter.toUnit", countQuery = "select count(unitConverter) from UnitConverter unitConverter") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select unitConverter from UnitConverter unitConverter left join fetch unitConverter.fromUnit left join fetch unitConverter.toUnit") + List findAllWithEagerRelationships(); + + @Override + @Query("select unitConverter from UnitConverter unitConverter left join fetch unitConverter.fromUnit left join fetch unitConverter.toUnit where unitConverter.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom EmissionFactorRepository methods */ + /* *************************************** */ + @Query("select unitConverter from UnitConverter unitConverter where unitConverter.fromUnit.id =:fromUnitId and unitConverter.toUnit.id =:toUnitId") + List findAllFromAndTo(@Param("fromUnitId") Long fromUnitId, @Param("toUnitId") Long toUnitId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/UnitRepository.java b/src/main/java/com/oguerreiro/resilient/repository/UnitRepository.java new file mode 100644 index 0000000..f69990f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/UnitRepository.java @@ -0,0 +1,49 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Unit; + +/** + * Spring Data JPA repository for the Unit entity. + */ +@Repository +public interface UnitRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select unit from Unit unit left join fetch unit.unitType", countQuery = "select count(unit) from Unit unit") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select unit from Unit unit left join fetch unit.unitType") + List findAllWithEagerRelationships(); + + @Override + @Query("select unit from Unit unit left join fetch unit.unitType where unit.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom UnitRepository methods */ + /* ***************************** */ + @Query("select unit from Unit unit left join fetch unit.unitType unitYpe where unitType.id=:unitTypeId") + List findAllByTypeWithToOneRelationships(@Param("unitTypeId") Long unitTypeId); + + Optional findOneByCodeOrSymbol(String code, String symbol); + + @Query(""" + select unit from Unit unit, VariableUnits vu where vu.variable.id=:variableId AND vu.unit=unit + """) + List findAllUnitsAllowedForVariable(@Param("variableId") Long variableId); + + @Query(""" + select unit from Unit unit, Variable v where v.id=:variableId AND v.baseUnit=unit + """) + List findBaseUnitForVariable(@Param("variableId") Long variableId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/UnitTypeRepository.java b/src/main/java/com/oguerreiro/resilient/repository/UnitTypeRepository.java new file mode 100644 index 0000000..74b9d52 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/UnitTypeRepository.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.UnitType; + +/** + * Spring Data JPA repository for the UnitType entity. + */ +@Repository +public interface UnitTypeRepository extends ResilientJpaRepository, UnitTypeRepositoryExtended { +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/UnitTypeRepositoryExtended.java b/src/main/java/com/oguerreiro/resilient/repository/UnitTypeRepositoryExtended.java new file mode 100644 index 0000000..e4993b2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/UnitTypeRepositoryExtended.java @@ -0,0 +1,7 @@ +package com.oguerreiro.resilient.repository; + +import com.oguerreiro.resilient.domain.UnitType; + +public interface UnitTypeRepositoryExtended { + void saveInNewTransaction(UnitType unitType); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/UserRepository.java b/src/main/java/com/oguerreiro/resilient/repository/UserRepository.java new file mode 100644 index 0000000..f6d338b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/UserRepository.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.repository; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.User; + +/** + * Spring Data JPA repository for the {@link User} entity. + */ +@Repository +public interface UserRepository extends JpaRepository { + String USERS_BY_LOGIN_CACHE = "usersByLogin"; + + String USERS_BY_EMAIL_CACHE = "usersByEmail"; + + Optional findOneByActivationKey(String activationKey); + + List findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant dateTime); + + Optional findOneByResetKey(String resetKey); + + Optional findOneByEmailIgnoreCase(String email); + + Optional findOneByLogin(String login); + + @EntityGraph(attributePaths = { "authorities", "securityGroup", "securityGroup.securityGroupPermissions", + "securityGroup.securityGroupPermissions.securityResource" }) + @Cacheable(cacheNames = USERS_BY_LOGIN_CACHE) + Optional findOneWithAuthoritiesByLogin(String login); + + @EntityGraph(attributePaths = { "authorities", "securityGroup", "securityGroup.securityGroupPermissions", + "securityGroup.securityGroupPermissions.securityResource" }) + @Cacheable(cacheNames = USERS_BY_EMAIL_CACHE) + Optional findOneWithAuthoritiesByEmailIgnoreCase(String email); + + Page findAllByIdNotNullAndActivatedIsTrue(Pageable pageable); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/VariableCategoryRepository.java b/src/main/java/com/oguerreiro/resilient/repository/VariableCategoryRepository.java new file mode 100644 index 0000000..bb780e8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/VariableCategoryRepository.java @@ -0,0 +1,41 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.VariableCategory; + +/** + * Spring Data JPA repository for the VariableCategory entity. + */ +@Repository +public interface VariableCategoryRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select variableCategory from VariableCategory variableCategory left join fetch variableCategory.variableScope", countQuery = "select count(variableCategory) from VariableCategory variableCategory") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select variableCategory from VariableCategory variableCategory left join fetch variableCategory.variableScope") + List findAllWithEagerRelationships(); + + @Override + @Query("select variableCategory from VariableCategory variableCategory left join fetch variableCategory.variableScope where variableCategory.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom VariableCategoryRepository methods */ + /* ***************************************** */ + @Query("select cat from VariableCategory cat left join fetch cat.variableScope scope where scope.id=:scopeId order by cat.name") + List findAllByScopeWithToOneRelationships(@Param("scopeId") Long scopeId); + + @Query("select cat from VariableCategory cat left join fetch cat.variableScope scope where scope.id=:scopeId AND (NOT(cat.hiddenForFactor) OR cat.hiddenForFactor IS NULL) order by cat.name") + List findByVariableScopeIdAndHiddenForFactorIsFalseOrHiddenForFactorIsNullOrderByNameAsc( + Long scopeId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/VariableClassRepository.java b/src/main/java/com/oguerreiro/resilient/repository/VariableClassRepository.java new file mode 100644 index 0000000..22def75 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/VariableClassRepository.java @@ -0,0 +1,35 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.VariableClass; + +/** + * Spring Data JPA repository for the VariableClass entity. + */ +@Repository +public interface VariableClassRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select variableClass from VariableClass variableClass", countQuery = "select count(variableClass) from VariableClass variableClass") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select variableClass from VariableClass variableClass") + List findAllWithEagerRelationships(); + + @Override + @Query("select variableClass from VariableClass variableClass where variableClass.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + @Query("select class from VariableClass class, Variable v where class.variableClassType=v.variableClassType and v.id = :variableId order by class.name") + List getAllForVariable(@Param("variableId") Long variableId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/VariableClassTypeRepository.java b/src/main/java/com/oguerreiro/resilient/repository/VariableClassTypeRepository.java new file mode 100644 index 0000000..9a4484c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/VariableClassTypeRepository.java @@ -0,0 +1,12 @@ +package com.oguerreiro.resilient.repository; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.VariableClassType; + +/** + * Spring Data JPA repository for the UnitType entity. + */ +@Repository +public interface VariableClassTypeRepository extends ResilientJpaRepository { +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/VariableRepository.java b/src/main/java/com/oguerreiro/resilient/repository/VariableRepository.java new file mode 100644 index 0000000..6c9fb01 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/VariableRepository.java @@ -0,0 +1,56 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.Variable; + +/** + * Spring Data JPA repository for the Variable entity. + */ +@Repository +public interface VariableRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select variable from Variable variable left join fetch variable.baseUnit left join fetch variable.variableScope left join fetch variable.variableCategory left join fetch variable.variableUnits", countQuery = "select count(variable) from Variable variable") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select variable from Variable variable left join fetch variable.baseUnit left join fetch variable.variableScope left join fetch variable.variableCategory left join fetch variable.variableUnits") + List findAllWithEagerRelationships(); + + @Override + @Query("select variable from Variable variable left join fetch variable.baseUnit left join fetch variable.variableScope left join fetch variable.variableCategory left join fetch variable.variableUnits where variable.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + /* Custom VariableRepository methods */ + /* ********************************* */ + @Query("select variable from Variable variable left join fetch variable.baseUnit left join fetch variable.variableScope left join fetch variable.variableCategory left join fetch variable.variableUnits where variable.code =:code") + Optional findOneByCodeWithToOneRelationships(@Param("code") String code); + + @Query("select variable from Variable variable where variable.output = true and (variable.outputSingle is null OR variable.outputSingle = false)") + List findAllOutputNotSingle(); + + @Query("select variable from Variable variable where variable.output = true and variable.outputSingle = true") + List findAllOutputSingle(); + + @Query(""" + select variable + from Variable variable + where variable.input = true + and variable.inputMode IN (com.oguerreiro.resilient.domain.enumeration.InputMode.MANUAL, com.oguerreiro.resilient.domain.enumeration.InputMode.ANY) + order by variable.name + """) + List findAllForManualInput(); + + @Query("SELECT v FROM Variable v WHERE v.input = :input AND v.output = :output ORDER BY v.code") + Stream streamAll(@Param("input") Boolean input, @Param("output") Boolean output); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/VariableScopeRepository.java b/src/main/java/com/oguerreiro/resilient/repository/VariableScopeRepository.java new file mode 100644 index 0000000..e7178b6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/VariableScopeRepository.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.VariableScope; + +/** + * Spring Data JPA repository for the VariableScope entity. + */ +@Repository +public interface VariableScopeRepository extends ResilientJpaRepository { + /* Custom VariableScopeRepository methods */ + /* ***************************************** */ + List findByHiddenForDataIsFalseOrHiddenForDataIsNullOrderByCodeAsc(); + + List findByHiddenForFactorIsFalseOrHiddenForFactorIsNullOrderByCodeAsc(); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/VariableUnitsRepository.java b/src/main/java/com/oguerreiro/resilient/repository/VariableUnitsRepository.java new file mode 100644 index 0000000..447254c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/VariableUnitsRepository.java @@ -0,0 +1,32 @@ +package com.oguerreiro.resilient.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.domain.VariableUnits; + +/** + * Spring Data JPA repository for the VariableUnits entity. + */ +@Repository +public interface VariableUnitsRepository extends ResilientJpaRepository { + /* Interface ResilientJpaRepository eager fetching Override methods */ + /* **************************************************************** */ + @Override + @Query(value = "select variableUnits from VariableUnits variableUnits left join fetch variableUnits.variable left join fetch variableUnits.unit", countQuery = "select count(variableUnits) from VariableUnits variableUnits") + Page findAllWithEagerRelationships(Pageable pageable); + + @Override + @Query("select variableUnits from VariableUnits variableUnits left join fetch variableUnits.variable left join fetch variableUnits.unit") + List findAllWithEagerRelationships(); + + @Override + @Query("select variableUnits from VariableUnits variableUnits left join fetch variableUnits.variable left join fetch variableUnits.unit where variableUnits.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataRepositoryExtendedImpl.java b/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataRepositoryExtendedImpl.java new file mode 100644 index 0000000..2d1bb20 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataRepositoryExtendedImpl.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.repository.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.repository.InputDataRepositoryExtended; + +import jakarta.persistence.EntityManager; + +@Repository +public class InputDataRepositoryExtendedImpl implements InputDataRepositoryExtended { + @Autowired + private EntityManager entityManager; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveInNewTransaction(InputData inputData) { + // Persist the entity in a new transaction + entityManager.persist(inputData); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataUploadLogRepositoryEntendedImpl.java b/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataUploadLogRepositoryEntendedImpl.java new file mode 100644 index 0000000..e832875 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataUploadLogRepositoryEntendedImpl.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.repository.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.InputDataUploadLog; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepositoryEntended; + +import jakarta.persistence.EntityManager; + +@Repository +public class InputDataUploadLogRepositoryEntendedImpl implements InputDataUploadLogRepositoryEntended { + @Autowired + private EntityManager entityManager; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveInNewTransaction(InputDataUploadLog inputDataUploadLog) { + // Persist the entity in a new transaction + entityManager.persist(inputDataUploadLog); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataUploadRepositoryEntendedImpl.java b/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataUploadRepositoryEntendedImpl.java new file mode 100644 index 0000000..6206fd5 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/impl/InputDataUploadRepositoryEntendedImpl.java @@ -0,0 +1,27 @@ +package com.oguerreiro.resilient.repository.impl; + +import org.hibernate.LockMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.repository.InputDataUploadRepositoryEntended; + +import jakarta.persistence.EntityManager; + +@Repository +public class InputDataUploadRepositoryEntendedImpl implements InputDataUploadRepositoryEntended { + @Autowired + private EntityManager entityManager; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public InputDataUpload updateInNewTransaction(InputDataUpload inputDataUpload) { + // Persist the entity in a new transaction + inputDataUpload = entityManager.merge(inputDataUpload); + return inputDataUpload; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/impl/OutputDataRepositoryExtendedImpl.java b/src/main/java/com/oguerreiro/resilient/repository/impl/OutputDataRepositoryExtendedImpl.java new file mode 100644 index 0000000..fdaa7e0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/impl/OutputDataRepositoryExtendedImpl.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.repository.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.repository.OutputDataRepositoryExtended; + +import jakarta.persistence.EntityManager; + +@Repository +public class OutputDataRepositoryExtendedImpl implements OutputDataRepositoryExtended { + @Autowired + private EntityManager entityManager; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveInNewTransaction(OutputData outputData) { + // Persist the entity in a new transaction + entityManager.persist(outputData); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/impl/UnitTypeRepositoryExtendedImpl.java b/src/main/java/com/oguerreiro/resilient/repository/impl/UnitTypeRepositoryExtendedImpl.java new file mode 100644 index 0000000..b01246f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/impl/UnitTypeRepositoryExtendedImpl.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.repository.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.UnitType; +import com.oguerreiro.resilient.repository.UnitTypeRepositoryExtended; + +import jakarta.persistence.EntityManager; + +@Repository +public class UnitTypeRepositoryExtendedImpl implements UnitTypeRepositoryExtended { + @Autowired + private EntityManager entityManager; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveInNewTransaction(UnitType unitType) { + // Persist the entity in a new transaction + entityManager.persist(unitType); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/package-info.java b/src/main/java/com/oguerreiro/resilient/repository/package-info.java new file mode 100644 index 0000000..40f689f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * Repository layer. + */ +package com.oguerreiro.resilient.repository; diff --git a/src/main/java/com/oguerreiro/resilient/repository/security/SecurityGroupPermissionRepository.java b/src/main/java/com/oguerreiro/resilient/repository/security/SecurityGroupPermissionRepository.java new file mode 100644 index 0000000..70dbc63 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/security/SecurityGroupPermissionRepository.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.repository.security; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.repository.ResilientJpaRepository; +import com.oguerreiro.resilient.security.resillient.SecurityGroupPermission; + +/** + * Spring Data JPA repository for the SecurityGroupPermission entity. + */ +@SuppressWarnings("unused") +@Repository +public interface SecurityGroupPermissionRepository extends ResilientJpaRepository { + @Query(""" + SELECT s + FROM SecurityGroupPermission s + WHERE s.securityGroup.id=:securityGroupId + ORDER BY s.securityResource.name + """) + List findBySecurityGroupId(@Param("securityGroupId") Long securityGroupId); +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/security/SecurityGroupRepository.java b/src/main/java/com/oguerreiro/resilient/repository/security/SecurityGroupRepository.java new file mode 100644 index 0000000..b5b2b64 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/security/SecurityGroupRepository.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.repository.security; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.repository.ResilientJpaRepository; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; + +/** + * Spring Data JPA repository for the SecurityGroup entity. + */ +@SuppressWarnings("unused") +@Repository +public interface SecurityGroupRepository extends ResilientJpaRepository { + @Override + @Query("select securityGroup from SecurityGroup securityGroup left join fetch securityGroup.securityGroupPermissions permissions left join fetch securityGroup.securityGroupPermissions.securityResource resource where securityGroup.id =:id") + Optional findOneWithEagerRelationships(@Param("id") Long id); + + @Query("select securityGroup from SecurityGroup securityGroup left join fetch securityGroup.securityGroupPermissions permissions left join fetch securityGroup.securityGroupPermissions.securityResource resource where securityGroup.code =:code") + Optional findOneByCodeWithEagerRelationships(@Param("code") String code); + +} diff --git a/src/main/java/com/oguerreiro/resilient/repository/security/SecurityResourceRepository.java b/src/main/java/com/oguerreiro/resilient/repository/security/SecurityResourceRepository.java new file mode 100644 index 0000000..c31cd9d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/repository/security/SecurityResourceRepository.java @@ -0,0 +1,17 @@ +package com.oguerreiro.resilient.repository.security; + +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import com.oguerreiro.resilient.repository.ResilientJpaRepository; +import com.oguerreiro.resilient.security.resillient.SecurityResource; + +/** + * Spring Data JPA repository for the SecurityResource entity. + */ +@SuppressWarnings("unused") +@Repository +public interface SecurityResourceRepository extends ResilientJpaRepository { + Optional findByCode(String code); +} diff --git a/src/main/java/com/oguerreiro/resilient/security/AuthoritiesConstants.java b/src/main/java/com/oguerreiro/resilient/security/AuthoritiesConstants.java new file mode 100644 index 0000000..a22a092 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/AuthoritiesConstants.java @@ -0,0 +1,15 @@ +package com.oguerreiro.resilient.security; + +/** + * Constants for Spring Security authorities. + */ +public final class AuthoritiesConstants { + + public static final String ADMIN = "ROLE_ADMIN"; + + public static final String USER = "ROLE_USER"; + + public static final String ANONYMOUS = "ROLE_ANONYMOUS"; + + private AuthoritiesConstants() {} +} diff --git a/src/main/java/com/oguerreiro/resilient/security/CrypterUtils.java b/src/main/java/com/oguerreiro/resilient/security/CrypterUtils.java new file mode 100644 index 0000000..6c6d5bc --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/CrypterUtils.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.security; + +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class CrypterUtils { + private static final String ALGORITHM = "AES"; + + public static String AESKeyGenerate() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM); + keyGen.init(256); // Use AES-256 for strong security + SecretKey secretKey = keyGen.generateKey(); + + // Encode key as a Base64 string (to store in config) + String encodedKey = Base64.getEncoder().encodeToString(secretKey.getEncoded()); + + return encodedKey; + } + + public static String encrypt(String data, String base64Key) throws Exception { + byte[] decodedKey = Base64.getDecoder().decode(base64Key); + SecretKeySpec secretKey = new SecretKeySpec(decodedKey, ALGORITHM); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] encryptedBytes = cipher.doFinal(data.getBytes()); + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + public static String decrypt(String encryptedPassword, String secretKey) throws Exception { + byte[] decodedKey = Base64.getDecoder().decode(secretKey); + SecretKey key = new SecretKeySpec(decodedKey, 0, decodedKey.length, ALGORITHM); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, key); + + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedPassword)); + return new String(decryptedBytes); + } +} \ No newline at end of file diff --git a/src/main/java/com/oguerreiro/resilient/security/DomainUserDetailsService.java b/src/main/java/com/oguerreiro/resilient/security/DomainUserDetailsService.java new file mode 100644 index 0000000..daf631d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/DomainUserDetailsService.java @@ -0,0 +1,115 @@ +package com.oguerreiro.resilient.security; + +import java.util.List; +import java.util.Locale; + +import org.hibernate.Hibernate; +import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Authority; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.repository.security.SecurityGroupRepository; +import com.oguerreiro.resilient.security.custom.ResilientUserDetails; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; + +/** + * Authenticate a user from the database. + */ +@Component("userDetailsService") +public class DomainUserDetailsService implements UserDetailsService { + + private final Logger log = LoggerFactory.getLogger(DomainUserDetailsService.class); + + private final UserRepository userRepository; + private final OrganizationRepository organizationRepository; + private final SecurityGroupRepository securityGroupRepository; + + public DomainUserDetailsService(UserRepository userRepository, OrganizationRepository organizationRepository, + SecurityGroupRepository securityGroupRepository) { + this.userRepository = userRepository; + this.organizationRepository = organizationRepository; + this.securityGroupRepository = securityGroupRepository; + } + + /* + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(final String login) { + log.debug("Authenticating {}", login); + + if (new EmailValidator().isValid(login, null)) { + return userRepository.findOneWithAuthoritiesByEmailIgnoreCase(login).map( + user -> createSpringSecurityUser(login, user)).orElseThrow( + () -> new UsernameNotFoundException("User with email " + login + " was not found in the database")); + } + + String lowercaseLogin = login.toLowerCase(Locale.ENGLISH); + return userRepository.findOneWithAuthoritiesByLogin(lowercaseLogin).map( + user -> createSpringSecurityUser(lowercaseLogin, user)).orElseThrow( + () -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the database")); + } + */ + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(final String login) { + log.debug("Authenticating {}", login); + + if (new EmailValidator().isValid(login, null)) { + return userRepository.findOneWithAuthoritiesByEmailIgnoreCase(login).map( + user -> createResilientSecurityUser(login, user)).orElseThrow( + () -> new UsernameNotFoundException("User with email " + login + " was not found in the database")); + } + + String lowercaseLogin = login.toLowerCase(Locale.ENGLISH); + return userRepository.findOneWithAuthoritiesByLogin(lowercaseLogin).map( + user -> createResilientSecurityUser(lowercaseLogin, user)).orElseThrow( + () -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the database")); + } + + private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, + User user) { + if (!user.isActivated()) { + throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated"); + } + List grantedAuthorities = user.getAuthorities().stream().map(Authority::getName).map( + SimpleGrantedAuthority::new).toList(); + + return new org.springframework.security.core.userdetails.User(user.getLogin(), user.getPassword(), + grantedAuthorities); + } + + private ResilientUserDetails createResilientSecurityUser(String lowercaseLogin, User user) { + if (!user.isActivated()) { + throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated"); + } + + List grantedAuthorities = user.getAuthorities().stream().map(Authority::getName).map( + SimpleGrantedAuthority::new).toList(); + + //Find the user in the organization + Organization parentOrg = null; + Organization person = organizationRepository.findOneByUser(user).orElse(null); + if (person != null) { + parentOrg = person.getParent(); + Hibernate.initialize(parentOrg); // explicitly loads the proxy. I need to guarantee that this is NEVER a uninitialized proxy + } + // Find the SecurityGroup for permissions + // SecurityGroup securityGroup = this.securityGroupRepository.findOneWithEagerRelationships(4L).orElse(null); // TODO: Replace with more functional code, this is just testing + SecurityGroup securityGroup = user.getSecurityGroup(); + + return new ResilientUserDetails(user.getLogin(), user.getPassword(), grantedAuthorities, securityGroup, parentOrg, + user.getLangKey()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/PersistentTokenRememberMeServices.java b/src/main/java/com/oguerreiro/resilient/security/PersistentTokenRememberMeServices.java new file mode 100644 index 0000000..95d6001 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/PersistentTokenRememberMeServices.java @@ -0,0 +1,219 @@ +package com.oguerreiro.resilient.security; + +import com.oguerreiro.resilient.domain.PersistentToken; +import com.oguerreiro.resilient.repository.PersistentTokenRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.time.LocalDate; +import java.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.rememberme.*; +import org.springframework.stereotype.Service; +import tech.jhipster.config.JHipsterProperties; +import tech.jhipster.security.PersistentTokenCache; +import tech.jhipster.security.RandomUtil; + +/** + * Custom implementation of Spring Security's RememberMeServices. + *

+ * Persistent tokens are used by Spring Security to automatically log in users. + *

+ * This is a specific implementation of Spring Security's remember-me authentication, but it is much + * more powerful than the standard implementations: + *

    + *
  • It allows a user to see the list of his currently opened sessions, and invalidate them
  • + *
  • It stores more information, such as the IP address and the user agent, for audit purposes
  • + *
  • When a user logs out, only his current session is invalidated, and not all of his sessions
  • + *
+ *

+ * Please note that it allows the use of the same token for 5 seconds, and this value stored in a specific + * cache during that period. This is to allow concurrent requests from the same user: otherwise, two + * requests being sent at the same time could invalidate each other's token. + *

+ * This is inspired by: + *

+ *

+ * The main algorithm comes from Spring Security's {@code PersistentTokenBasedRememberMeServices}, but this class + * couldn't be cleanly extended. + */ +@Service +public class PersistentTokenRememberMeServices extends AbstractRememberMeServices { + + private final Logger log = LoggerFactory.getLogger(PersistentTokenRememberMeServices.class); + + // Token is valid for one month + private static final int TOKEN_VALIDITY_DAYS = 31; + + private static final int TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * TOKEN_VALIDITY_DAYS; + + private static final long UPGRADED_TOKEN_VALIDITY_MILLIS = 5000l; + + private final PersistentTokenCache upgradedTokenCache; + + private final PersistentTokenRepository persistentTokenRepository; + + private final UserRepository userRepository; + + public PersistentTokenRememberMeServices( + JHipsterProperties jHipsterProperties, + org.springframework.security.core.userdetails.UserDetailsService userDetailsService, + PersistentTokenRepository persistentTokenRepository, + UserRepository userRepository + ) { + super(jHipsterProperties.getSecurity().getRememberMe().getKey(), userDetailsService); + this.persistentTokenRepository = persistentTokenRepository; + this.userRepository = userRepository; + upgradedTokenCache = new PersistentTokenCache<>(UPGRADED_TOKEN_VALIDITY_MILLIS); + } + + @Override + protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { + synchronized (this) { // prevent 2 authentication requests from the same user in parallel + String login = null; + UpgradedRememberMeToken upgradedToken = upgradedTokenCache.get(cookieTokens[0]); + if (upgradedToken != null) { + login = upgradedToken.getUserLoginIfValid(cookieTokens); + log.debug("Detected previously upgraded login token for user '{}'", login); + } + + if (login == null) { + PersistentToken token = getPersistentToken(cookieTokens); + login = token.getUser().getLogin(); + + // Token also matches, so login is valid. Update the token value, keeping the *same* series number. + log.debug("Refreshing persistent login token for user '{}', series '{}'", login, token.getSeries()); + token.setTokenDate(LocalDate.now()); + token.setTokenValue(RandomUtil.generateRandomAlphanumericString()); + token.setIpAddress(request.getRemoteAddr()); + token.setUserAgent(request.getHeader("User-Agent")); + try { + persistentTokenRepository.saveAndFlush(token); + } catch (DataAccessException e) { + log.error("Failed to update token: ", e); + throw new RememberMeAuthenticationException("Autologin failed due to data access problem", e); + } + addCookie(token, request, response); + upgradedTokenCache.put(cookieTokens[0], new UpgradedRememberMeToken(cookieTokens, login)); + } + return getUserDetailsService().loadUserByUsername(login); + } + } + + @Override + protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { + String login = successfulAuthentication.getName(); + + log.debug("Creating new persistent login for user {}", login); + PersistentToken token = userRepository + .findOneByLogin(login) + .map(u -> { + PersistentToken t = new PersistentToken(); + t.setSeries(RandomUtil.generateRandomAlphanumericString()); + t.setUser(u); + t.setTokenValue(RandomUtil.generateRandomAlphanumericString()); + t.setTokenDate(LocalDate.now()); + t.setIpAddress(request.getRemoteAddr()); + t.setUserAgent(request.getHeader("User-Agent")); + return t; + }) + .orElseThrow(() -> new UsernameNotFoundException("User " + login + " was not found in the database")); + try { + persistentTokenRepository.saveAndFlush(token); + addCookie(token, request, response); + } catch (DataAccessException e) { + log.error("Failed to save persistent token ", e); + } + } + + /** + * When logout occurs, only invalidate the current token, and not all user sessions. + *

+ * The standard Spring Security implementations are too basic: they invalidate all tokens for the + * current user, so when he logs out from one browser, all his other sessions are destroyed. + * + * @param request the request. + * @param response the response. + * @param authentication the authentication. + */ + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String rememberMeCookie = extractRememberMeCookie(request); + if (rememberMeCookie != null && rememberMeCookie.length() != 0) { + try { + String[] cookieTokens = decodeCookie(rememberMeCookie); + PersistentToken token = getPersistentToken(cookieTokens); + persistentTokenRepository.deleteById(token.getSeries()); + } catch (InvalidCookieException ice) { + log.info("Invalid cookie, no persistent token could be deleted", ice); + } catch (RememberMeAuthenticationException rmae) { + log.debug("No persistent token found, so no token could be deleted", rmae); + } + } + super.logout(request, response, authentication); + } + + /** + * Validate the token and return it. + */ + private PersistentToken getPersistentToken(String[] cookieTokens) { + if (cookieTokens.length != 2) { + throw new InvalidCookieException( + "Cookie token did not contain " + 2 + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'" + ); + } + String presentedSeries = cookieTokens[0]; + String presentedToken = cookieTokens[1]; + Optional optionalToken = persistentTokenRepository.findById(presentedSeries); + if (!optionalToken.isPresent()) { + // No series match, so we can't authenticate using this cookie + throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries); + } + PersistentToken token = optionalToken.orElseThrow(); + // We have a match for this user/series combination + log.info("presentedToken={} / tokenValue={}", presentedToken, token.getTokenValue()); + if (!presentedToken.equals(token.getTokenValue())) { + // Token doesn't match series value. Delete this session and throw an exception. + persistentTokenRepository.deleteById(token.getSeries()); + throw new CookieTheftException("Invalid remember-me token (Series/token) mismatch. Implies previous " + "cookie theft attack."); + } + if (token.getTokenDate().plusDays(TOKEN_VALIDITY_DAYS).isBefore(LocalDate.now())) { + persistentTokenRepository.deleteById(token.getSeries()); + throw new RememberMeAuthenticationException("Remember-me login has expired"); + } + return token; + } + + private void addCookie(PersistentToken token, HttpServletRequest request, HttpServletResponse response) { + setCookie(new String[] { token.getSeries(), token.getTokenValue() }, TOKEN_VALIDITY_SECONDS, request, response); + } + + private static class UpgradedRememberMeToken implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String[] upgradedToken; + + private final String userLogin; + + UpgradedRememberMeToken(String[] upgradedToken, String userLogin) { + this.upgradedToken = upgradedToken; + this.userLogin = userLogin; + } + + String getUserLoginIfValid(String[] currentToken) { + if (currentToken[0].equals(this.upgradedToken[0]) && currentToken[1].equals(this.upgradedToken[1])) { + return this.userLogin; + } + return null; + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/SecurityUtils.java b/src/main/java/com/oguerreiro/resilient/security/SecurityUtils.java new file mode 100644 index 0000000..c24da01 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/SecurityUtils.java @@ -0,0 +1,121 @@ +package com.oguerreiro.resilient.security; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.security.custom.ResilientUserDetails; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; + +/** + * Utility class for Spring Security. + */ +public final class SecurityUtils { + + private SecurityUtils() { + } + + /** + * Get the login of the current user. + * + * @return the login of the current user. + */ + public static Optional getCurrentUserLogin() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + return Optional.ofNullable(extractPrincipal(securityContext.getAuthentication())); + } + + private static String extractPrincipal(Authentication authentication) { + if (authentication == null) { + return null; + } else if (authentication.getPrincipal() instanceof UserDetails springSecurityUser) { + return springSecurityUser.getUsername(); + } else if (authentication.getPrincipal() instanceof String s) { + return s; + } + return null; + } + + /** + * Check if a user is authenticated. + * + * @return true if the user is authenticated, false otherwise. + */ + public static boolean isAuthenticated() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && getAuthorities(authentication).noneMatch(AuthoritiesConstants.ANONYMOUS::equals); + } + + /** + * Checks if the current user has any of the authorities. + * + * @param authorities the authorities to check. + * @return true if the current user has any of the authorities, false otherwise. + */ + public static boolean hasCurrentUserAnyOfAuthorities(String... authorities) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return (authentication != null + && getAuthorities(authentication).anyMatch(authority -> Arrays.asList(authorities).contains(authority))); + } + + /** + * Checks if the current user has none of the authorities. + * + * @param authorities the authorities to check. + * @return true if the current user has none of the authorities, false otherwise. + */ + public static boolean hasCurrentUserNoneOfAuthorities(String... authorities) { + return !hasCurrentUserAnyOfAuthorities(authorities); + } + + /** + * Checks if the current user has a specific authority. + * + * @param authority the authority to check. + * @return true if the current user has the authority, false otherwise. + */ + public static boolean hasCurrentUserThisAuthority(String authority) { + return hasCurrentUserAnyOfAuthorities(authority); + } + + public static SecurityGroup getCurrentUserSecurityGroup() { + if (!isAuthenticated()) { + return null; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Object details = authentication.getPrincipal(); + if (details == null || !(details instanceof ResilientUserDetails)) { + return null; + } + ResilientUserDetails resilientDetails = (ResilientUserDetails) details; + + return resilientDetails.getSecurityGroup(); + } + + public static Organization getCurrentUserParentOrganization() { + if (!isAuthenticated()) { + return null; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Object details = authentication.getPrincipal(); + if (details == null || !(details instanceof ResilientUserDetails)) { + return null; + } + ResilientUserDetails resilientDetails = (ResilientUserDetails) details; + + return resilientDetails.getParentOrganization(); + } + + private static Stream getAuthorities(Authentication authentication) { + return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/SpringSecurityAuditorAware.java b/src/main/java/com/oguerreiro/resilient/security/SpringSecurityAuditorAware.java new file mode 100644 index 0000000..d3cfce4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/SpringSecurityAuditorAware.java @@ -0,0 +1,18 @@ +package com.oguerreiro.resilient.security; + +import com.oguerreiro.resilient.config.Constants; +import java.util.Optional; +import org.springframework.data.domain.AuditorAware; +import org.springframework.stereotype.Component; + +/** + * Implementation of {@link AuditorAware} based on Spring Security. + */ +@Component +public class SpringSecurityAuditorAware implements AuditorAware { + + @Override + public Optional getCurrentAuditor() { + return Optional.of(SecurityUtils.getCurrentUserLogin().orElse(Constants.SYSTEM)); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/UserNotActivatedException.java b/src/main/java/com/oguerreiro/resilient/security/UserNotActivatedException.java new file mode 100644 index 0000000..7599938 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/UserNotActivatedException.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.security; + +import org.springframework.security.core.AuthenticationException; + +/** + * This exception is thrown in case of a not activated user trying to authenticate. + */ +public class UserNotActivatedException extends AuthenticationException { + + private static final long serialVersionUID = 1L; + + public UserNotActivatedException(String message) { + super(message); + } + + public UserNotActivatedException(String message, Throwable t) { + super(message, t); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/custom/CustomPermissionEvaluator.java b/src/main/java/com/oguerreiro/resilient/security/custom/CustomPermissionEvaluator.java new file mode 100644 index 0000000..2037fe4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/custom/CustomPermissionEvaluator.java @@ -0,0 +1,60 @@ +package com.oguerreiro.resilient.security.custom; + +import java.io.Serializable; + +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.core.Authentication; + +public class CustomPermissionEvaluator implements PermissionEvaluator { + + // This method is used to check permissions on a domain object (e.g., User or Resource) + @Override + public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { + // Retrieve the current class instance from ThreadLocal + Object currentInstance = ResilientSecurityContextAspect.getCurrentTargetInstance(); + + if (authentication == null) { + return false; // If no authentication context exists, deny permission + } + + String username = authentication.getName(); // Retrieve the current username + String requiredPermission = (String) permission; // Retrieve the required permission (e.g., 'READ', 'WRITE') + + // Custom logic for permission evaluation, e.g., checking if the user has the required permission + if ("READ".equals(requiredPermission)) { + return hasReadPermission(username); // Custom logic for 'READ' permission + } else if ("WRITE".equals(requiredPermission)) { + return hasWritePermission(username); // Custom logic for 'WRITE' permission + } + + return true; + } + + // This method is used to check permissions on an object based on its ID and type + @Override + public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, + Object permission) { + // Retrieve the current class instance from ThreadLocal + Object currentInstance = ResilientSecurityContextAspect.getCurrentTargetInstance(); + + if (authentication == null) { + return false; // If no authentication context exists, deny permission + } + + // If permission is based on a specific target (e.g., a specific resource or entity), you can implement logic here + String username = authentication.getName(); + return hasReadPermission(username); // For example, checking if the user has read permission + } + + // Custom logic for checking "READ" permission + private boolean hasReadPermission(String username) { + // For example, check if the username starts with "admin" to allow "READ" permission + return username.startsWith("admin"); + } + + // Custom logic for checking "WRITE" permission + private boolean hasWritePermission(String username) { + // For example, check if the username starts with "admin" to allow "WRITE" permission + return username.startsWith("admin"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/custom/ResilientBasePermission.java b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientBasePermission.java new file mode 100644 index 0000000..8d8b87d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientBasePermission.java @@ -0,0 +1,5 @@ +package com.oguerreiro.resilient.security.custom; + +public enum ResilientBasePermission { + CREATE, READ, UPDATE, DELETE, EXECUTE, CUSTOM; +} diff --git a/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurity.java b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurity.java new file mode 100644 index 0000000..72eaa39 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurity.java @@ -0,0 +1,247 @@ +package com.oguerreiro.resilient.security.custom; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; +import com.oguerreiro.resilient.security.resillient.SecurityGroupPermission; +import com.oguerreiro.resilient.security.resillient.SecurityPermission; +import com.oguerreiro.resilient.service.AbstractResilientService; +import com.oguerreiro.resilient.web.rest.AbstractResilientResource; + +/** + * For this to work, an Aspect must run before @PreAuthorize. + * Use this in methods like this: + * + *

+ * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess()")
+ * 
+ * + * OR + * + *
+ * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess('CUSTOM_AUTHORITY')")
+ * 
+ * + * @see ResilientSecurityContextAspect + */ +@Component +public class ResilientSecurity { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + /** + * Resilient security annotation that forces a base permission type of {@link ResilientBasePermission#CUSTOM}, and + * supplies a customPermission code to evaluate user permissions. + * + * Usage: + * + *
+   * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess('CUSTOM_AUTHORITY')")
+   * 
+ * + * @param customPermission + * @return + */ + public boolean canAccess(String customPermission) { + return this.internalCanAccess(customPermission, null); + } + + /** + * Resilient security annotation that forces a base permission type of {@link ResilientBasePermission#CUSTOM}, and + * supplies a customPermission code to evaluate user permissions. + * + * Usage: + * + *
+   * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess('CUSTOM_AUTHORITY')")
+   * 
+ * + * @param customPermission + * @return + */ + public boolean canAccess(String customPermission, String customResource) { + return this.internalCanAccess(customPermission, customResource); + } + + /** + * Resilient security annotation that infer's the base permission type of {@link ResilientBasePermission permission + * type} from the {@link ResilientSecurityContext}. + * + * Usage: + * + *
+   * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess()")
+   * 
+ * + * In a simply way, this automatically sets the {@link ResilientBasePermission} for the secured + * {@link AbstractResilientResource} method names. + * + * @param customPermission + * @return + */ + public boolean canAccess() { + return this.internalCanAccess(null, null); + } + + private boolean internalCanAccess(String customPermission, String customResource) { + ResilientSecurityContext targetInvocationContext = ResilientSecurityContextAspect.getCurrentTargetInstance(); + if (targetInvocationContext == null) { + // Must have the invocation context. The secured method and its class instance. Without it, can't evaluate security, + log.error("Unauthorized. No ResilientSecurityContext was provided."); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + // Authentication must exist. + log.error("Unauthorized access to . Authentication doesn't exist in the context."); + return false; + } else if (!authentication.isAuthenticated()) { + // And must be authenticated + log.error("Unauthorized. Authentication is NOT authenticated."); + return false; + } + + //Distinguish the target instance + Object targetInstance = targetInvocationContext.getTargetInstance(); + if (targetInstance == null) { + // Must have the targetInstance filled in the invocation context. Something is WRONG in this request. + // TODO : Log this event + return false; + } + + // Admin user doesn't need a SecurityGroup and Have access to EVERYTHING + if (SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.ADMIN)) { + return true; + } + + // Must have a SecurityGroup assigned + SecurityGroup group = SecurityUtils.getCurrentUserSecurityGroup(); + if (group == null) { + return false; + } + + // Calculate user permission allowance + SecurityPermission allowedPermission = SecurityPermission.NONE; + if (targetInstance instanceof AbstractResilientResource) { + // Resource endpoint secured method + allowedPermission = this.evaluateResourceAccess(authentication, (AbstractResilientResource) targetInstance, + targetInvocationContext.getTargetMethodName(), customPermission, customResource, group); + } else if (targetInstance instanceof AbstractResilientService) { + // Service secured method + } else { + // Unknowned target object. The security method annotation (canAccess()) is used in a ilegal place + // It must have customPermission && customResource defined + allowedPermission = evaluateAccess(ResilientBasePermission.valueOf(customPermission), customResource, group); + } + + // Evaluate the result. NOTE: HIERARCHY will be re-check in the Service, for more complex business security validations + if (allowedPermission.equals(SecurityPermission.ALL) || allowedPermission.equals(SecurityPermission.HIERARCHY)) { + return true; + } + + return false; + } + + private SecurityPermission evaluateResourceAccess(Authentication authentication, AbstractResilientResource resource, + String endpointMethodName, String customPermission, String customResource, SecurityGroup group) { + + // By method name, infer the needed permission + ResilientBasePermission neededPermission = null; + + if (customPermission != null) { + //This is a custom permission, defined in security annotation + neededPermission = ResilientBasePermission.valueOf(customPermission); + } else { + //Try to automatically infer the permission from method name + neededPermission = this.inferPermission(endpointMethodName); + } + + // Evaluate permission using the {group} + String neededResource; + if (customResource != null) { + neededResource = customResource; + } else { + neededResource = resource.getDomainClass().getName(); + } + + return this.evaluateAccess(neededPermission, neededResource, group); + } + + private SecurityPermission evaluateAccess(ResilientBasePermission neededPermission, String resource, + SecurityGroup group) { + SecurityPermission allowedSecurityPermission = SecurityPermission.NONE; + + // Evaluate permission using the {group} + SecurityGroupPermission permission = this.findPermission(resource, group); + + if (permission == null) { + // If the method is secured, the user/group must define a permission for it. + return allowedSecurityPermission; + } + + switch (neededPermission) { + case CREATE: { + allowedSecurityPermission = permission.getCreatePermission(); + break; + } + case READ: { + allowedSecurityPermission = permission.getReadPermission(); + break; + } + case UPDATE: { + allowedSecurityPermission = permission.getUpdatePermission(); + break; + } + case DELETE: { + allowedSecurityPermission = permission.getDeletePermission(); + break; + } + default: + allowedSecurityPermission = SecurityPermission.NONE; + break; + } + + return allowedSecurityPermission; + } + + private SecurityGroupPermission findPermission(String resourceCode, SecurityGroup securityGroup) { + return securityGroup.getSecurityGroupPermissions().stream().filter( + (perm) -> perm.getSecurityResource().getCode().equals(resourceCode)).findAny().orElse(null); + } + + private ResilientBasePermission inferPermission(String endpointMethodName) { + ResilientBasePermission neededPermission = null; + + switch (endpointMethodName) { + case AbstractResilientResource.ENDPOINT_METHOD_NAME_CREATE: { + neededPermission = ResilientBasePermission.CREATE; + break; + } + case AbstractResilientResource.ENDPOINT_METHOD_NAME_READ: + case AbstractResilientResource.ENDPOINT_METHOD_NAME_LIST: + case AbstractResilientResource.ENDPOINT_METHOD_NAME_CRITERIA_LIST: + neededPermission = ResilientBasePermission.READ; + break; + case AbstractResilientResource.ENDPOINT_METHOD_NAME_DELETE: { + neededPermission = ResilientBasePermission.DELETE; + break; + } + case AbstractResilientResource.ENDPOINT_METHOD_NAME_UPDATE: + case AbstractResilientResource.ENDPOINT_METHOD_NAME_PARTIAL: + neededPermission = ResilientBasePermission.CREATE; + break; + default: + // This means that the invoked endpoint its not implemented at the level of AbstractResilientResource. + // Its a custom endpoint. For this, a custom permission code must be provided in the annotation + neededPermission = ResilientBasePermission.CUSTOM; + } + + return neededPermission; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityContext.java b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityContext.java new file mode 100644 index 0000000..c23bfe6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityContext.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.security.custom; + +public class ResilientSecurityContext { + private final Object targetInstance; + private final String targetMethodName; + private final Object[] targetArgs; + + public ResilientSecurityContext(Object targetInstance, String targetMethodName, Object[] targetArgs) { + this.targetInstance = targetInstance; + this.targetMethodName = targetMethodName; + this.targetArgs = targetArgs; + } + + public Object getTargetInstance() { + return this.targetInstance; + } + + public String getTargetMethodName() { + return this.targetMethodName; + } + + public Object[] getTargetArgs() { + return this.targetArgs; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityContextAspect.java b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityContextAspect.java new file mode 100644 index 0000000..a599424 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityContextAspect.java @@ -0,0 +1,38 @@ +package com.oguerreiro.resilient.security.custom; + +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Order(1) // Needed to guarantee that it runs BEFORE spring security @PreAuthorize evaluation +public class ResilientSecurityContextAspect { + + private static final ThreadLocal currentTargetInstance = new ThreadLocal<>(); + + // Inject needed info into a ThreadLocal variable + @Before("execution(@org.springframework.security.access.prepost.PreAuthorize * *.*(..))") + public void captureTargetInstance(org.aspectj.lang.JoinPoint joinPoint) { + Object target = joinPoint.getTarget(); // Get the class instance where the method is being called + + currentTargetInstance.set( + new ResilientSecurityContext(target, joinPoint.getSignature().getName(), joinPoint.getArgs())); // Store the instance in ThreadLocal + } + + // Clear the ThreadLocal variable, for safety + @After("execution(@org.springframework.security.access.prepost.PreAuthorize * *.*(..))") + public void clearTargetInstance() { + currentTargetInstance.remove(); // Clean up after method execution + } + + public static ResilientSecurityContext getCurrentTargetInstance() { + return currentTargetInstance.get(); // Retrieve the stored instance + } + + public static void clear() { + currentTargetInstance.remove(); // Clear the stored instance after the request is processed + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityResourceConfig.java b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityResourceConfig.java new file mode 100644 index 0000000..f7c33a1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientSecurityResourceConfig.java @@ -0,0 +1,27 @@ +package com.oguerreiro.resilient.security.custom; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.oguerreiro.resilient.security.resillient.SecurityPermission; +import com.oguerreiro.resilient.security.resillient.SecurityResourceType; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ResilientSecurityResourceConfig { + public static final String DEFAULT_CODE = "@CLASSNAME@"; + + String code() default DEFAULT_CODE; + + SecurityResourceType type() default SecurityResourceType.DOMAIN; + + SecurityPermission defaultCreatePermission() default SecurityPermission.NONE; + + SecurityPermission defaultReadPermission() default SecurityPermission.NONE; + + SecurityPermission defaultUpdatePermission() default SecurityPermission.NONE; + + SecurityPermission defaultDeletePermission() default SecurityPermission.NONE; +} \ No newline at end of file diff --git a/src/main/java/com/oguerreiro/resilient/security/custom/ResilientUserDetails.java b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientUserDetails.java new file mode 100644 index 0000000..a127a2e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/custom/ResilientUserDetails.java @@ -0,0 +1,43 @@ +package com.oguerreiro.resilient.security.custom; + +import java.util.Collection; + +import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; + +public class ResilientUserDetails extends User implements AuthenticatedPrincipal { + + private final SecurityGroup securityGroup; // The security group permissions for the user + private final Organization parentOrganization; // The parent organization where the user is within the organogram + private final String langKey; //User language + + public ResilientUserDetails(String username, String password, Collection authorities, + SecurityGroup securityGroup, Organization parentOrganization, String langKey) { + super(username, password, true, true, true, true, authorities); + this.securityGroup = securityGroup; + this.parentOrganization = parentOrganization; + this.langKey = langKey; + } + + public SecurityGroup getSecurityGroup() { + return securityGroup; + } + + public String getLangKey() { + return langKey; + } + + public Organization getParentOrganization() { + return parentOrganization; + } + + @Override + public String getName() { + return super.getUsername(); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/security/package-info.java b/src/main/java/com/oguerreiro/resilient/security/package-info.java new file mode 100644 index 0000000..e91d46b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/package-info.java @@ -0,0 +1,4 @@ +/** + * Application security utilities. + */ +package com.oguerreiro.resilient.security; diff --git a/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityGroup.java b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityGroup.java new file mode 100644 index 0000000..f03057d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityGroup.java @@ -0,0 +1,119 @@ +package com.oguerreiro.resilient.security.resillient; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.AbstractResilientLongIdEntity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * SecurityGroup + */ +@Entity +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Table(name = "security_group") +public class SecurityGroup extends AbstractResilientLongIdEntity { + private static final long serialVersionUID = 1L; + + /** + * User defined code. Must be unique. + */ + @NotNull + @Size(min = 3) + @Column(name = "code", nullable = false, unique = true) + private String code; + + /** + * Description. + */ + @NotNull + @Size(min = 3) + @Column(name = "name") + private String name; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "securityGroup", cascade = CascadeType.ALL, orphanRemoval = true) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "securityGroup" }, allowSetters = true) + private List securityGroupPermissions = new ArrayList<>(); + + public String getCode() { + return code; + } + + public SecurityGroup code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public List getSecurityGroupPermissions() { + return securityGroupPermissions; + } + + public SecurityGroup securityGroupPermissions(List securityGroupPermissions) { + this.setSecurityGroupPermissions(securityGroupPermissions); + return this; + } + + public void setSecurityGroupPermissions(List securityGroupPermissions) { + if (this.securityGroupPermissions != null) { + this.securityGroupPermissions.forEach((v) -> v.setSecurityGroup(null)); + } + if (securityGroupPermissions != null) { + securityGroupPermissions.forEach((v) -> v.setSecurityGroup(this)); + } + this.securityGroupPermissions = securityGroupPermissions; + } + + public SecurityGroup addSecurityGroupPermission(SecurityGroupPermission securityGroupPermission) { + this.securityGroupPermissions.add(securityGroupPermission); + securityGroupPermission.setSecurityGroup(this); + return this; + } + + public SecurityGroup removeSecurityGroupPermission(SecurityGroupPermission securityGroupPermission) { + this.securityGroupPermissions.remove(securityGroupPermission); + securityGroupPermission.setSecurityGroup(null); + return this; + } + + public String getName() { + return name; + } + + public SecurityGroup name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + //@formatter:off + @Override + public String toString() { + return "SecurityGroup{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityGroupPermission.java b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityGroupPermission.java new file mode 100644 index 0000000..c9d622f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityGroupPermission.java @@ -0,0 +1,145 @@ +package com.oguerreiro.resilient.security.resillient; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.oguerreiro.resilient.domain.AbstractResilientLongIdEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +/** + * SecurityGroupPermission + */ +@Entity +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Table(name = "security_group_permission") +public class SecurityGroupPermission extends AbstractResilientLongIdEntity { + private static final long serialVersionUID = 1L; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties(value = { "securityGroupPermissions" }, allowSetters = true) + private SecurityGroup securityGroup; + + @ManyToOne(fetch = FetchType.LAZY) + private SecurityResource securityResource; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "create_permission", nullable = false) + private SecurityPermission createPermission; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "read_permission", nullable = false) + private SecurityPermission readPermission; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "update_permission", nullable = false) + private SecurityPermission updatePermission; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "delete_permission", nullable = false) + private SecurityPermission deletePermission; + + public SecurityGroup getSecurityGroup() { + return this.securityGroup; + } + + public void setSecurityGroup(SecurityGroup securityGroup) { + this.securityGroup = securityGroup; + } + + public SecurityGroupPermission securityGroup(SecurityGroup securityGroup) { + this.setSecurityGroup(securityGroup); + return this; + } + + public SecurityResource getSecurityResource() { + return this.securityResource; + } + + public void setSecurityResource(SecurityResource securityResource) { + this.securityResource = securityResource; + } + + public SecurityGroupPermission securityResource(SecurityResource securityResource) { + this.setSecurityResource(securityResource); + return this; + } + + public SecurityPermission getCreatePermission() { + return createPermission; + } + + public SecurityGroupPermission createPermission(SecurityPermission createPermission) { + this.setCreatePermission(createPermission); + return this; + } + + public void setCreatePermission(SecurityPermission createPermission) { + this.createPermission = createPermission; + } + + public SecurityPermission getReadPermission() { + return readPermission; + } + + public SecurityGroupPermission readPermission(SecurityPermission readPermission) { + this.setReadPermission(readPermission); + return this; + } + + public void setReadPermission(SecurityPermission readPermission) { + this.readPermission = readPermission; + } + + public SecurityPermission getUpdatePermission() { + return updatePermission; + } + + public SecurityGroupPermission updatePermission(SecurityPermission updatePermission) { + this.setUpdatePermission(updatePermission); + return this; + } + + public void setUpdatePermission(SecurityPermission updatePermission) { + this.updatePermission = updatePermission; + } + + public SecurityPermission getDeletePermission() { + return deletePermission; + } + + public SecurityGroupPermission deletePermission(SecurityPermission deletePermission) { + this.setDeletePermission(deletePermission); + return this; + } + + public void setDeletePermission(SecurityPermission deletePermission) { + this.deletePermission = deletePermission; + } + + //@formatter:off + @Override + public String toString() { + return "SecurityGroup{" + + "id=" + getId() + + ", createPermission='" + getCreatePermission() + "'" + + ", readPermission='" + getReadPermission() + "'" + + ", updatePermission='" + getUpdatePermission() + "'" + + ", deletePermission='" + getDeletePermission() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityPermission.java b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityPermission.java new file mode 100644 index 0000000..557ce57 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityPermission.java @@ -0,0 +1,5 @@ +package com.oguerreiro.resilient.security.resillient; + +public enum SecurityPermission { + ALL, NONE, HIERARCHY, +} diff --git a/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityResource.java b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityResource.java new file mode 100644 index 0000000..d1da9f9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityResource.java @@ -0,0 +1,167 @@ +package com.oguerreiro.resilient.security.resillient; + +import com.oguerreiro.resilient.domain.AbstractResilientLongIdEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * SecurityResource + */ +@Entity +@Table(name = "security_resource") +public class SecurityResource extends AbstractResilientLongIdEntity { + private static final long serialVersionUID = 1L; + + /** + * User defined code. Must be unique. + */ + @NotNull + @Size(min = 3) + @Column(name = "code", nullable = false, unique = true) + private String code; + + @Column(name = "name", nullable = false, unique = true) + private String name; + + /** + * The type of resource + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + private SecurityResourceType type; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "default_create_permission", nullable = false) + private SecurityPermission defaultCreatePermission; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "default_read_permission", nullable = false) + private SecurityPermission defaultReadPermission; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "default_update_permission", nullable = false) + private SecurityPermission defaultUpdatePermission; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "default_delete_permission", nullable = false) + private SecurityPermission defaultDeletePermission; + + public String getCode() { + return code; + } + + public SecurityResource code(String code) { + this.setCode(code); + return this; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public SecurityResource name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + public SecurityResourceType getType() { + return type; + } + + public SecurityResource type(SecurityResourceType type) { + this.setType(type); + return this; + } + + public void setType(SecurityResourceType type) { + this.type = type; + } + + public SecurityPermission getDefaultCreatePermission() { + return defaultCreatePermission; + } + + public SecurityResource defaultCreatePermission(SecurityPermission defaultCreatePermission) { + this.setDefaultCreatePermission(defaultCreatePermission); + return this; + } + + public void setDefaultCreatePermission(SecurityPermission defaultCreatePermission) { + this.defaultCreatePermission = defaultCreatePermission; + } + + public SecurityPermission getDefaultReadPermission() { + return defaultReadPermission; + } + + public SecurityResource defaultReadPermission(SecurityPermission defaultReadPermission) { + this.setDefaultReadPermission(defaultReadPermission); + return this; + } + + public void setDefaultReadPermission(SecurityPermission defaultReadPermission) { + this.defaultReadPermission = defaultReadPermission; + } + + public SecurityPermission getDefaultUpdatePermission() { + return defaultUpdatePermission; + } + + public SecurityResource defaultUpdatePermission(SecurityPermission defaultUpdatePermission) { + this.setDefaultUpdatePermission(defaultUpdatePermission); + return this; + } + + public void setDefaultUpdatePermission(SecurityPermission defaultUpdatePermission) { + this.defaultUpdatePermission = defaultUpdatePermission; + } + + public SecurityPermission getDefaultDeletePermission() { + return defaultDeletePermission; + } + + public SecurityResource defaultDeletePermission(SecurityPermission defaultDeletePermission) { + this.setDefaultDeletePermission(defaultDeletePermission); + return this; + } + + public void setDefaultDeletePermission(SecurityPermission defaultDeletePermission) { + this.defaultDeletePermission = defaultDeletePermission; + } + + //@formatter:off + @Override + public String toString() { + return "SecurityResource{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", type='" + getType() + "'" + + ", defaultCreatePermission='" + getDefaultCreatePermission() + "'" + + ", defaultReadPermission='" + getDefaultReadPermission() + "'" + + ", defaultUpdatePermission='" + getDefaultUpdatePermission() + "'" + + ", defaultDeletePermission='" + getDefaultDeletePermission() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityResourceType.java b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityResourceType.java new file mode 100644 index 0000000..5c697f1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/resillient/SecurityResourceType.java @@ -0,0 +1,5 @@ +package com.oguerreiro.resilient.security.resillient; + +public enum SecurityResourceType { + DOMAIN, ACTIVITY, CUSTOM +} diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/DevMockIdpStarter.java b/src/main/java/com/oguerreiro/resilient/security/saml2/DevMockIdpStarter.java new file mode 100644 index 0000000..6ecf8a1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/saml2/DevMockIdpStarter.java @@ -0,0 +1,106 @@ +package com.oguerreiro.resilient.security.saml2; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; + +/** + * In a DEV environment a mock-idp might be needed for SAMLv2 authentication. This will automatically run the mock-idp + * daemon for testing.
+ * The mock-idp is a separate NodeJS project in : src\main\mock-idp. More info about this in the readme file, inside the + * project dir.
+ * Sometimes it can be useful to run MOCK-IDP separately: + *
    + *
  1. disable it in application.yml ({@code resilient.mock-idp. enabled: true})
  2. + *
  3. open a command line console
  4. + *
  5. goto the mock-idp dir
  6. + *
  7. execute: {@code node idp.js}
  8. + *
+ *
+ * To run this, configuration must be added to application<-dev>.yml: + * + *
+ * 
+spring:
+  security:
+    saml2:     # ADDED to support SAMLv2 authentication to IDP.
+      relyingparty:
+        registration:
+          mock-idp:
+            assertingparty:
+              entity-id: http://localhost:3000/saml/metadata
+              single-sign-on:
+                url: http://localhost:3000/saml/sso
+              single-logout:
+                url: http://localhost:3000/saml/slo
+              verification:
+                credentials:
+                  - certificate-location: classpath:saml/idp-public.cert
+              want-authn-signed: false    # Validate signature in entire message response (true-validates/false-doesn't validate)
+              want-assertion-signed: true # Validate signature in assertions message response (true-validates/false-doesn't validate)
+            signing:
+              credentials:
+                - private-key-location: classpath:saml/private.key
+                  certificate-location: classpath:saml/public.cert
+resilient:
+  mock-idp:
+    enabled: true
+    path: classpath:mock-idp/idp.js
+ * 
+ * 
+ */ +@Component +@Profile("dev") +public class DevMockIdpStarter { + + @Value("${resilient.mock-idp.enabled:false}") + private Boolean mockIdpEnabled; + + @Value("${resilient.mock-idp.path:}") //Optional path. + private String mockIdpPath; + + @Autowired + private ResourceLoader resourceLoader; + + @PostConstruct + public void startMockIdp() throws IOException { + // If not set, ignore and DON'T run the SAML2 mock-idp + if (!mockIdpEnabled || mockIdpPath == null || mockIdpPath.isBlank()) + return; + + String realMockIdpPath = getRealMockIdpPath(); + + ProcessBuilder builder = new ProcessBuilder("node", "--trace-warnings", realMockIdpPath); + builder.inheritIO(); // Optional: pipes output to Spring console + + Path path = Paths.get(realMockIdpPath); + Path realMockIdpPathParent = path.getParent(); + builder.directory(realMockIdpPathParent.toFile()); // working directory + + Process process = builder.start(); + + // Optional: add a shutdown hook to stop it on JVM shutdown + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + process.destroy(); + })); + } + + private String getRealMockIdpPath() throws IOException { + Resource resource = resourceLoader.getResource(mockIdpPath); + + if (!resource.exists()) { + throw new IOException("File not found: " + mockIdpPath); + } + + return new String(resource.getFile().toPath().toString()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/ResilientSaml2Properties.java b/src/main/java/com/oguerreiro/resilient/security/saml2/ResilientSaml2Properties.java new file mode 100644 index 0000000..4542fab --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/saml2/ResilientSaml2Properties.java @@ -0,0 +1,240 @@ +package com.oguerreiro.resilient.security.saml2; + +import java.util.List; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "resilient.security.saml2") +public class ResilientSaml2Properties { + + private boolean enabled; + private String idpId; + private String baseUrl; + private String successUrl; + private String failureUrl; + private RelyingParty relyingparty; + + public static class RelyingParty { + private Map registration; + + public Map getRegistration() { + return registration; + } + + public void setRegistration(Map registration) { + this.registration = registration; + } + } + + public static class Registration { + private AssertingParty assertingparty; + private Signing signing; + + public AssertingParty getAssertingparty() { + return assertingparty; + } + + public void setAssertingparty(AssertingParty assertingparty) { + this.assertingparty = assertingparty; + } + + public Signing getSigning() { + return signing; + } + + public void setSigning(Signing signing) { + this.signing = signing; + } + } + + public static class AssertingParty { + private String entityId; + private UrlConfig singleSignOn; + private UrlConfig singleLogout; + private Verification verification; + private boolean wantAuthnSigned; + private boolean wantAssertionSigned; + + // Getters and Setters + + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public UrlConfig getSingleSignOn() { + return singleSignOn; + } + + public void setSingleSignOn(UrlConfig singleSignOn) { + this.singleSignOn = singleSignOn; + } + + public UrlConfig getSingleLogout() { + return singleLogout; + } + + public void setSingleLogout(UrlConfig singleLogout) { + this.singleLogout = singleLogout; + } + + public Verification getVerification() { + return verification; + } + + public void setVerification(Verification verification) { + this.verification = verification; + } + + public boolean isWantAuthnSigned() { + return wantAuthnSigned; + } + + public void setWantAuthnSigned(boolean wantAuthnSigned) { + this.wantAuthnSigned = wantAuthnSigned; + } + + public boolean isWantAssertionSigned() { + return wantAssertionSigned; + } + + public void setWantAssertionSigned(boolean wantAssertionSigned) { + this.wantAssertionSigned = wantAssertionSigned; + } + } + + public static class Signing { + private List credentials; + + public List getCredentials() { + return credentials; + } + + public void setCredentials(List credentials) { + this.credentials = credentials; + } + + public static class Credential { + private String privateKeyLocation; + private String certificateLocation; + + public String getPrivateKeyLocation() { + return privateKeyLocation; + } + + public void setPrivateKeyLocation(String privateKeyLocation) { + this.privateKeyLocation = privateKeyLocation; + } + + public String getCertificateLocation() { + return certificateLocation; + } + + public void setCertificateLocation(String certificateLocation) { + this.certificateLocation = certificateLocation; + } + } + } + + public static class Verification { + private List credentials; + + public List getCredentials() { + return credentials; + } + + public void setCredentials(List credentials) { + this.credentials = credentials; + } + + public static class Credential { + private String certificateLocation; + + public String getCertificateLocation() { + return certificateLocation; + } + + public void setCertificateLocation(String certificateLocation) { + this.certificateLocation = certificateLocation; + } + } + } + + public static class UrlConfig { + private String url; + // Add this map for query-parameters + private Map queryParameters; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Map getQueryParameters() { + return queryParameters; + } + + public void setQueryParameters(Map queryParameters) { + this.queryParameters = queryParameters; + } + } + + // Root-level Getters/Setters + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getIdpId() { + return idpId; + } + + public void setIdpId(String idpId) { + this.idpId = idpId; + } + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getSuccessUrl() { + return successUrl; + } + + public void setSuccessUrl(String successUrl) { + this.successUrl = successUrl; + } + + public String getFailureUrl() { + return failureUrl; + } + + public void setFailureUrl(String failureUrl) { + this.failureUrl = failureUrl; + } + + public RelyingParty getRelyingparty() { + return relyingparty; + } + + public void setRelyingparty(RelyingParty relyingparty) { + this.relyingparty = relyingparty; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java new file mode 100644 index 0000000..1f31979 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java @@ -0,0 +1,176 @@ +package com.oguerreiro.resilient.security.saml2; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.security.SecurityGroupRepository; +import com.oguerreiro.resilient.security.custom.ResilientUserDetails; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class Saml2AuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler { + + private static final String ATTR_SECURITYGROUP = "security_group"; + private static final String ATTR_EMAIL = "email"; + private static final String ATTR_ORGANIZATION = "organization_code"; + private static final String ATTR_ROLES = "roles"; + + @Autowired + private Environment environment; + + @Autowired + private OrganizationRepository organizationRepository; + + @Autowired + private SecurityGroupRepository securityGroupRepository; + + @Autowired + private ResilientSaml2Properties resilientSaml2Properties; + + /** + * AuthenticationSuccessHandler implementation + */ + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + String samlResponse = request.getParameter("SAMLResponse"); + String samlXMLResponse = Saml2ResponseLoggingFilter.decodeSamlResponse(samlResponse); + + // Read needed information about user security environment + Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + String username = principal.getName(); + String email = getPrincipalAttribute(principal, ATTR_EMAIL); + String securityGroupCode = getPrincipalAttribute(principal, ATTR_SECURITYGROUP); + String organizationCode = getPrincipalAttribute(principal, ATTR_ORGANIZATION); + List roles = principal.getAttribute(ATTR_ROLES); + Collection authorities; + + if (roles == null || roles.isEmpty()) { + authorities = authentication.getAuthorities(); + } else { + authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); + } + + Organization userOrganization = null; + SecurityGroup securityGroup = null; + + // Organization exists? + if (organizationCode == null || organizationCode.isBlank()) { + // TODO: Make this configurable + Long defaultOrg = 1L; + userOrganization = organizationRepository.findById(defaultOrg).orElse(null); + } else { + // Find or NULL + userOrganization = organizationRepository.findOneByCode(organizationCode).orElse(null); + } + if (userOrganization == null) { + this.invalidateLogin(request, response); + return; + } + + // SecurityGroup exists? + String defaultSecurityGroupCode = "default"; // TODO: Make this configurable + if (securityGroupCode == null || securityGroupCode.isBlank()) { + securityGroupCode = defaultSecurityGroupCode; + } + + // Find or fail + securityGroup = securityGroupRepository.findOneByCodeWithEagerRelationships(securityGroupCode).orElse(null); + + if (securityGroup == null) { + this.invalidateLogin(request, response); + return; + } + + // Create a ResilientUserDetails and replace Principal + ResilientUserDetails userdetails = new ResilientUserDetails(username, "MOCK-PWD", authorities, securityGroup, + userOrganization, "pt-PT"); + + Saml2Authentication newAuthentication = new Saml2Authentication(userdetails, samlXMLResponse, + authentication.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(newAuthentication); + + // This is a sugar-code when in development environment. + if (isDevProfileActive()) { + // If this is a mock-idp, it can provide the parameter 'SAMLDevEnvironmentUrl' + // that gives the base URL to use. This is because in DEV mode usually the + // Angular side runs in localhost:42000 but server-side is in localhost:8080. + // Without this, SAMLv2 authentication would end up in error redirecting the user to + // localhost:8080 (NOT the client-side) + // In PROD we don't need this, because the app url is the same + String successUrl = resilientSaml2Properties.getSuccessUrl(); + + if (successUrl != null && !successUrl.isBlank()) { + response.sendRedirect(successUrl); + } + } + } + + private void invalidateLogin(HttpServletRequest request, HttpServletResponse response) throws IOException { + // Clear context and invalidate session + SecurityContextHolder.clearContext(); + request.getSession(false).invalidate(); + + // Send error response or redirect + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid SAML attributes"); + } + + private boolean isDevProfileActive() { + return Arrays.asList(environment.getActiveProfiles()).contains("dev"); + } + + private String getPrincipalAttribute(Saml2AuthenticatedPrincipal principal, String attribute) { + List values = principal.getAttribute(attribute); + + if (values == null || values.isEmpty()) { + return null; + } + + return principal.getAttribute(attribute).get(0).toString(); + } + + /** + * AuthenticationFailureHandler implementation + */ + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + + // This is a sugar-code when in development environment. + if (isDevProfileActive()) { + // If this is a mock-idp, it can provide the parameter 'SAMLDevEnvironmentUrl' + // that gives the base URL to use. This is because in DEV mode usually the + // Angular side runs in localhost:42000 but server-side is in localhost:8080. + // Without this, SAMLv2 authentication would end up in error redirecting the user to + // localhost:8080 (NOT the client-side) + // In PROD we don't need this, because the app url is the same + String failureUrl = resilientSaml2Properties.getFailureUrl(); + + if (failureUrl != null && !failureUrl.isBlank()) { + response.sendRedirect(failureUrl); + } + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2CredentialLoader.java b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2CredentialLoader.java new file mode 100644 index 0000000..eeb4ac3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2CredentialLoader.java @@ -0,0 +1,42 @@ +package com.oguerreiro.resilient.security.saml2; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +@Component +public class Saml2CredentialLoader { + + private final ResourceLoader resourceLoader; + + public Saml2CredentialLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public X509Certificate loadCertificate(String location) throws Exception { + Resource resource = resourceLoader.getResource(location); + try (InputStream is = resource.getInputStream()) { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(is); + } + } + + public PrivateKey loadPrivateKey(String location) throws Exception { + Resource resource = resourceLoader.getResource(location); + String key = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + key = key.replaceAll("-----BEGIN (.*)-----", "").replaceAll("-----END (.*)-----", "").replaceAll("\\s", ""); + + byte[] decoded = Base64.getDecoder().decode(key); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded); + return KeyFactory.getInstance("RSA").generatePrivate(spec); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2RelyingPartyRegistrationRepository.java b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2RelyingPartyRegistrationRepository.java new file mode 100644 index 0000000..351c69c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2RelyingPartyRegistrationRepository.java @@ -0,0 +1,117 @@ +package com.oguerreiro.resilient.security.saml2; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.stream.Collectors; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; + +import com.oguerreiro.resilient.security.saml2.ResilientSaml2Properties.Registration; +import com.oguerreiro.resilient.security.saml2.ResilientSaml2Properties.UrlConfig; + +@Configuration +public class Saml2RelyingPartyRegistrationRepository { + + @Bean + @ConditionalOnProperty(name = "resilient.security.saml2.enabled", havingValue = "true") + public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository( + Saml2CredentialLoader saml2CredentialLoader, ResilientSaml2Properties resilientSaml2Properties) throws Exception { + + if (!resilientSaml2Properties.isEnabled()) + return null; + + // No registration ? + if (resilientSaml2Properties.getRelyingparty().getRegistration().size() == 0) + return null; + + // No IDP-ID ? + if (resilientSaml2Properties.getIdpId() == null || resilientSaml2Properties.getIdpId().isBlank()) { + return null; + } + + // Get the configured IDP + Registration registration = resilientSaml2Properties.getRelyingparty().getRegistration().get( + resilientSaml2Properties.getIdpId()); + // The intended IDP wasn't found + if (registration == null) + return null; + + // Load credentials + // YML : resilient.security.saml2.relyingparty.registration..assertingparty.verification.credentials.certificate-location: "classpath:saml/idp-public.cert" + String idpCertificateConf = registration.getAssertingparty().getVerification().getCredentials().get( + 0).getCertificateLocation(); + X509Certificate idpCertificate = saml2CredentialLoader.loadCertificate(idpCertificateConf); + Saml2X509Credential idpVerificationCredential = new Saml2X509Credential(idpCertificate, + Saml2X509Credential.Saml2X509CredentialType.VERIFICATION); + + // YML : resilient.security.saml2.relyingparty.registration..signing.credentials.certificate-location: "classpath:saml/public.cert" + String spCertificateConf = registration.getSigning().getCredentials().get(0).getCertificateLocation(); + X509Certificate spCertificate = saml2CredentialLoader.loadCertificate(spCertificateConf); + // YML : resilient.security.saml2.relyingparty.registration..signing.credentials.private-key-location: "classpath:saml/private.key" + String spPrivateKeyConf = registration.getSigning().getCredentials().get(0).getPrivateKeyLocation(); + PrivateKey spPrivateKey = saml2CredentialLoader.loadPrivateKey(spPrivateKeyConf); + Saml2X509Credential spSigningCredential = new Saml2X509Credential(spPrivateKey, spCertificate, + Saml2X509Credential.Saml2X509CredentialType.SIGNING); + + // Build RelyingPartyRegistration + //@formatter:off + // YML : resilient.security.saml2.relyingparty.registration., the name of the registration, in this case, mock-idp + String registrationIdConf = resilientSaml2Properties.getIdpId(); + // YML : resilient.security.saml2.relyingparty.registration..assertingparty.entity-id: "http://localhost:3000/saml/metadata" + String entityIdConf = registration.getAssertingparty().getEntityId(); + // YML : resilient.security.saml2.relyingparty.registration..assertingparty.single-sign-on.url: "http://localhost:3000/saml/sso" + String singleSignOnServiceLocationConf = this.buildUrlServiceLocation(registration.getAssertingparty().getSingleSignOn()); + // YML : resilient.security.saml2.relyingparty.registration..assertingparty.single-logout.url: "http://localhost:3000/saml/slo" + String singleLogoutServiceLocationConf = this.buildUrlServiceLocation(registration.getAssertingparty().getSingleLogout()); + // Build from configuration. Results in something like : "https://localhost:8443/saml2/service-provider-metadata/mock-idp" + String serviceProviderEntityIdConf = resilientSaml2Properties.getBaseUrl() + "/saml2/service-provider-metadata/" + registrationIdConf; + // Build from configuration. Results in something like : "https://localhost:8443/login/saml2/sso/mock-idp" + String assertionConsumerServiceLocationConf = resilientSaml2Properties.getBaseUrl() + "/login/saml2/sso/" + registrationIdConf; + RelyingPartyRegistration rpRegistration = RelyingPartyRegistration + .withRegistrationId(registrationIdConf) + .entityId(serviceProviderEntityIdConf) + .assertionConsumerServiceLocation(assertionConsumerServiceLocationConf) + .signingX509Credentials(c -> c.add(spSigningCredential)) + .assertingPartyDetails(party -> party + .entityId(entityIdConf) + .singleSignOnServiceLocation(singleSignOnServiceLocationConf) + .singleLogoutServiceLocation(singleLogoutServiceLocationConf) + .wantAuthnRequestsSigned(false) + .verificationX509Credentials(c -> c.add(idpVerificationCredential)) + ) + .build(); + //@formatter:on + + return new InMemoryRelyingPartyRegistrationRepository(rpRegistration); + } + + private String buildUrlServiceLocation(UrlConfig urlConfig) { + StringBuilder urlServiceLocation = new StringBuilder(); + urlServiceLocation.append(urlConfig.getUrl()); + + String queryString = ""; + if (urlConfig.getQueryParameters() != null && !urlConfig.getQueryParameters().isEmpty()) { + //@formatter:off + queryString = urlConfig.getQueryParameters().entrySet().stream() + .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue())) + .collect(Collectors.joining("&", "?", "")); + //@formatter:on + + urlServiceLocation.append(queryString); + } + + return urlServiceLocation.toString(); + } + + private static String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2ResponseLoggingFilter.java b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2ResponseLoggingFilter.java new file mode 100644 index 0000000..5275e53 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2ResponseLoggingFilter.java @@ -0,0 +1,57 @@ +package com.oguerreiro.resilient.security.saml2; + +import java.io.IOException; +import java.util.Base64; + +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class Saml2ResponseLoggingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if ("POST".equalsIgnoreCase(request.getMethod()) && request.getRequestURI().contains("/sso")) { + String samlResponse = request.getParameter("SAMLResponse"); + if (samlResponse != null) { + try { + byte[] decodedBytes = Base64.getDecoder().decode(samlResponse); + String decodedXml = new String(decodedBytes, java.nio.charset.StandardCharsets.UTF_8); + if (logger.isInfoEnabled()) { + // The logger.info() doesn't write linebreaks, it replaces "\n" for "_". VERY strange output in console and messes the XML. + // Decided to remove the linebreaks and output a clean XML, then I can copy and format the XML in Notepad++ + String cleanedXml = decodedXml.replaceAll("\\r?\\n", ""); + + logger.info("========== SAMLResponse XML =========="); + logger.info(cleanedXml); + logger.info("======================================"); + } + } catch (IllegalArgumentException e) { + logger.warn("Failed to decode SAMLResponse", e); + } + } + } else if ("/saml2/authenticate/mock-idp".equals(request.getRequestURI())) { + System.out.println("saml2"); + } + + filterChain.doFilter(request, response); + } + + public static String decodeSamlResponse(String samlResponse) { + if (samlResponse != null) { + try { + byte[] decodedBytes = Base64.getDecoder().decode(samlResponse); + String decodedXml = new String(decodedBytes, java.nio.charset.StandardCharsets.UTF_8); + return decodedXml; + } catch (IllegalArgumentException e) { + } + } + + return null; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/AbstractResilientService.java b/src/main/java/com/oguerreiro/resilient/service/AbstractResilientService.java new file mode 100644 index 0000000..f01b806 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/AbstractResilientService.java @@ -0,0 +1,218 @@ +package com.oguerreiro.resilient.service; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.repository.ResilientJpaRepository; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; +import com.oguerreiro.resilient.service.interceptors.AfterSaveServiceInterceptor; +import com.oguerreiro.resilient.service.interceptors.AfterUpdateServiceInterceptor; +import com.oguerreiro.resilient.service.interceptors.BeforeDeleteServiceInterceptor; +import com.oguerreiro.resilient.service.interceptors.BeforeSaveServiceInterceptor; +import com.oguerreiro.resilient.service.interceptors.BeforeUpdateServiceInterceptor; +import com.oguerreiro.resilient.service.mapper.ResilientEntityMapper; + +import jakarta.persistence.EntityManager; + +/** + * + * @param , the domain entity DTO class, eg. DomainDTO + * @param , the domain entity class type, eg. Domain + * @param , the domain entity key type, eg. Long + */ +@Service +@Transactional +public abstract class AbstractResilientService, T extends AbstractResilientEntity, ID extends Serializable> { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + private String domainSimpleName; + + private final ResilientJpaRepository domainRepository; + private final ResilientEntityMapper domainMapper; + private final Class domainClass; // This is a best practice to get the domain class. To infer from the generic (DOMAIN) is VERY hard. + + @Autowired + private EntityManager entityManager; + + protected AbstractResilientService(Class domainClass, ResilientJpaRepository domainRepository, + ResilientEntityMapper domainMapper) { + this.domainRepository = domainRepository; + this.domainMapper = domainMapper; + this.domainClass = domainClass; + + // Set some sugar values + this.domainSimpleName = this.domainClass.getSimpleName(); //Used in logging + } + + protected ResilientJpaRepository getRepository() { + return this.domainRepository; + } + + protected EntityManager getEntityManager() { + return this.entityManager; + } + + /** + * Save a Entity Domain. + * + * @param domainDTO the entity to save. + * @return the persisted entity. + */ + public DTO save(DTO domainDTO) { + log.debug("Request to save " + this.domainSimpleName + " : {}", domainDTO); + T domain = domainMapper.toEntity(domainDTO); + domain = this.callBeforeSaveInterceptor(domain); + domain = domainRepository.save(domain); + domain = this.callAfterSaveInterceptor(domain); + return domainMapper.toDto(domain); + } + + private T callBeforeSaveInterceptor(T domain) { + if (this instanceof BeforeSaveServiceInterceptor) { + return ((BeforeSaveServiceInterceptor) this).beforeSave(domain); + } + return domain; + } + + private T callAfterSaveInterceptor(T domain) { + if (this instanceof AfterSaveServiceInterceptor) { + return ((AfterSaveServiceInterceptor) this).afterSave(domain); + } + return domain; + } + + /** + * Update a Entity Domain. + * + * @param domainDTO the entity to save. + * @return the persisted entity. + */ + public DTO update(DTO domainDTO) { + log.debug("Request to update " + this.domainSimpleName + " : {}", domainDTO); + T domain = domainMapper.toEntity(domainDTO); + domain = this.callBeforeUpdateInterceptor(domain); + domain = domainRepository.save(domain); + domain = this.callAfterUpdateInterceptor(domain); + return domainMapper.toDto(domain); + } + + private T callBeforeUpdateInterceptor(T domain) { + if (this instanceof BeforeUpdateServiceInterceptor) { + return ((BeforeUpdateServiceInterceptor) this).beforeUpdate(domain); + } + return domain; + } + + private T callAfterUpdateInterceptor(T domain) { + if (this instanceof AfterUpdateServiceInterceptor) { + return ((AfterUpdateServiceInterceptor) this).afterUpdate(domain); + } + return domain; + } + + /** + * Partially update a entity of type . + * + * @param domainDTO the entity to update partially. + * @return the persisted entity. + */ + public Optional partialUpdate(DTO domainDTO) { + log.debug("Request to partially update " + this.domainSimpleName + " : {}", domainDTO); + + return domainRepository.findById(domainDTO.getId()).map(existingDomain -> { + domainMapper.partialUpdate(existingDomain, domainDTO); + + return existingDomain; + }).map(domainRepository::save).map(domainMapper::toDto); + } + + /** + * Get all the entities of type wrapped in a DTO of type . + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAll() { + log.debug("Request to get all " + this.domainSimpleName + "'s."); + return this.findAll(Sort.unsorted()); + } + + /** + * Get all the entities of type wrapped in a DTO of type . Sorted by the sort parameter. + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAll(Sort sort) { + log.debug("Request to get all " + this.domainSimpleName + "'s. Sorted by:", sort); + return domainRepository.findAll(sort).stream().map(domainMapper::toDto).collect( + Collectors.toCollection(LinkedList::new)); + } + + /** + * Get one entity by id, of type wrapped in a DTO of type . + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOne(ID id) { + log.debug("Request to get " + this.domainSimpleName + " : {}", id); + Optional entity = domainRepository.findById(id); + Optional dto = entity.map(domainMapper::toDto); + return dto; + } + + /** + * Delete the Entity Domain by id. + * + * @param id the id of the entity. + */ + public void delete(ID id) { + log.debug("Request to delete " + this.domainSimpleName + " : {}", id); + this.callBeforeDeleteInterceptor(id); + domainRepository.deleteById(id); + } + + private void callBeforeDeleteInterceptor(ID id) { + if (this instanceof BeforeDeleteServiceInterceptor) { + ((BeforeDeleteServiceInterceptor) this).beforeDelete(id); + } + } + + /* Eager relationships */ + /** + * Get all the emissionFactors with eager load of many-to-many relationships. + * + * @return the list of entities. + */ + public Page findAllWithEagerRelationships(Pageable pageable) { + log.debug("Request to findAllWithEagerRelationships : {}", pageable); + return domainRepository.findAllWithEagerRelationships(pageable).map(domainMapper::toDto); + } + + /** + * Get one emissionFactor by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOneWithEagerRelationships(ID id) { + log.debug("Request to findOneWithEagerRelationships : {}", id); + return domainRepository.findOneWithEagerRelationships(id).map(domainMapper::toDto); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/ContentPageService.java b/src/main/java/com/oguerreiro/resilient/service/ContentPageService.java new file mode 100644 index 0000000..bbc00da --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/ContentPageService.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.ContentPage; +import com.oguerreiro.resilient.repository.ContentPageRepository; +import com.oguerreiro.resilient.service.dto.ContentPageDTO; +import com.oguerreiro.resilient.service.mapper.ContentPageMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.ContentPage}. + */ +@Service +@Transactional +public class ContentPageService extends AbstractResilientService { + + public ContentPageService(ContentPageRepository contentPageRepository, ContentPageMapper contentPageMapper) { + super(ContentPage.class, contentPageRepository, contentPageMapper); + + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/DashboardComponentService.java b/src/main/java/com/oguerreiro/resilient/service/DashboardComponentService.java new file mode 100644 index 0000000..3e812a2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/DashboardComponentService.java @@ -0,0 +1,294 @@ +package com.oguerreiro.resilient.service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.poi.ooxml.POIXMLProperties; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.DashboardComponent; +import com.oguerreiro.resilient.domain.DashboardComponentDetail; +import com.oguerreiro.resilient.domain.DashboardComponentDetailValue; +import com.oguerreiro.resilient.domain.DashboardComponentOrganization; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView; +import com.oguerreiro.resilient.repository.DashboardComponentRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.service.dto.DashboardComponentDTO; +import com.oguerreiro.resilient.service.mapper.DashboardComponentMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.DashboardComponent}. + */ +@Service +@Transactional +public class DashboardComponentService + extends AbstractResilientService { + + private OrganizationRepository organizationRepository; + private DashboardComponentMapper dashboardComponentMapper; + + public DashboardComponentService(DashboardComponentRepository dashboardComponentRepository, + DashboardComponentMapper dashboardComponentMapper, OrganizationRepository organizationRepository) { + super(DashboardComponent.class, dashboardComponentRepository, dashboardComponentMapper); + + this.organizationRepository = organizationRepository; + this.dashboardComponentMapper = dashboardComponentMapper; + } + + /* + * Reads ALL orgs and data. Then:
+ *
    + *
  • organizationId == 1 - Then, group's the loaded data and builds a single Organization list values
  • + *
  • organizationId != 1 - Then, group's the loaded data and builds a Organization list values for NOVA (same when organizationId == 1). But also returns the given organizationId data.
  • + *
+ * + * TODO: Probably the best way to do this it's NOT loading all and sum in memory. The best is probably 2 separate query's, that already sum the values. + * And this solves another problem. Now I fixed the top organization ID. With this solution, I should build 2 list's of organization ID's:
+ *
    + *
  • From the given organizationId, search the TOP most Organization. Then build a list of ALL children. This will give the sum values for top org
  • + *
  • From the given organizationId, get the list of ALL children. This will give the sum values for the selected Org
  • + *
+ * This also enables the possibility to output all levels between top and the selected organizationId + */ + public Map> buildDashboardComponentView(Long organizationId, + Long dashboardComponentId, Long periodVersionId) { + List finalValues = new ArrayList(); + + // Step 1: Load all output data for the dashboardComponent + Long searchOrganizationId = null; //Forces to load all data + List values = this.getDashboardComponentRepository().loadAllDashboardComponentDetailValues( + searchOrganizationId, periodVersionId, dashboardComponentId); + + // Step 2: Group by variableId and sum values. To have NOVA org values + Organization novaOrg = this.organizationRepository.getReferenceById(1L); + Map mainOrgSummedValues = Collections.emptyMap(); + + //Group values + Map summedValues = values.stream().collect( + Collectors.groupingBy(DashboardComponentDetailValue::getDashboardComponentDetail, Collectors.mapping( + DashboardComponentDetailValue::getValue, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)))); + + // Sort the map by DashboardComponentDetailValue.getDashboardComponentDetail().getName() + // @formatter:off + mainOrgSummedValues = summedValues.entrySet() + .stream() + .sorted(Comparator.comparingInt(entry -> { + return entry.getKey().getIndexOrder(); + })) + .collect(Collectors.toMap( + (Map.Entry entry) -> entry.getKey(), + (Map.Entry entry) -> entry.getValue(), + (e1, e2) -> e1, // Merge function (not needed here) + LinkedHashMap::new // Preserve insertion order + )); + // @formatter:on + + // Step 3: Extract values for the selected organizationId (if any) + List filterOrgValues = null; + if (organizationId != 1) { + //Only if its NOT the main org + filterOrgValues = values.stream().filter(value -> value.getOwner().getId().equals(organizationId)).collect( + Collectors.toList()); + } + + // Step 4: Add both collections to finalValues + // #1 - Selected Org (must be the last one) + if (filterOrgValues != null) { + finalValues.addAll(filterOrgValues); + } + + // #2 - Main Org (must be the last one) + for (Map.Entry entry : mainOrgSummedValues.entrySet()) { + finalValues.add(new DashboardComponentDetailValue(entry.getKey(), entry.getKey().getTargetDashboardComponent(), + novaOrg, entry.getValue())); + } + + // Step 5: Group the list, into a MAP. Key=Organization; Value=List of DashboardComponentDetailValue + //@formatter:off + Map> mapValues = finalValues.stream() + .collect(Collectors.groupingBy( + value -> value.getOwner().getId().toString(), + LinkedHashMap::new, // Ensures LinkedHashMap is used + Collectors.toList() + )); + //@formatter:on + + // Step 6: Guarantee that TOP Org is last + if (mapValues.containsKey("1")) { + List value = mapValues.remove("1"); // Remove first + mapValues.put("1", value); // Reinsert at the end + } + + // Get DashboardComponent and check the Organization's list + List orgs = this.getDashboardComponentRepository().getReferenceById( + dashboardComponentId).getDashboardComponentOrganizations(); + if (orgs != null && !orgs.isEmpty()) { + List keysToRemove = new ArrayList<>(); + for (String key : mapValues.keySet()) { + boolean anyMatch = orgs.stream().anyMatch(org -> org.getOrganization().getId().equals(Long.valueOf(key))); + if (!anyMatch) { + keysToRemove.add(key); + } + } + keysToRemove.forEach(key -> mapValues.remove(key)); + } + + return mapValues; + } + + private DashboardComponentRepository getDashboardComponentRepository() { + return (DashboardComponentRepository) super.getRepository(); + } + + @Transactional(readOnly = true) + public List findAllActive() { + log.debug("Request to get all active DashboardComponent's."); + return this.getDashboardComponentRepository().findAllActive().stream().map(dashboardComponentMapper::toDto).collect( + Collectors.toCollection(LinkedList::new)); + } + + @Transactional(readOnly = true) + public List findAllActiveForView(DashboardComponentView dashboardComponentView) { + log.debug("Request to findAllActiveForView."); + return this.getDashboardComponentRepository().findAllActiveForView(dashboardComponentView).stream().map( + dashboardComponentMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + } + + @Transactional(readOnly = true) + public byte[] export(Long organizationId, Long dashboardComponentId, Long periodversionId) { + DashboardComponent dashboardComponent = this.getDashboardComponentRepository().getReferenceById( + dashboardComponentId); + + //Build a Workbook + try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("Export"); + + // Get the TOP component. This will be used as a template definer + Map> mapValues = this.buildDashboardComponentView(organizationId, + dashboardComponentId, periodversionId); + + // Set title + String pageTitle = ""; + String pageSubTitle = ""; + if (dashboardComponent.getView().equals(DashboardComponentView.EMISSIONS)) { + pageTitle = "Emissões de Gases com Efeito de Estufa"; + pageSubTitle = "t CO2e"; + } else if (dashboardComponent.getView().equals(DashboardComponentView.INDICATORS)) { + pageTitle = "Indicadores"; + } + + int c = 0; + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue(pageTitle); + headerRow = sheet.createRow(1); + headerRow.createCell(0).setCellValue(pageSubTitle); + + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + headerRow = sheet.createRow(2); + headerRow.createCell(0).setCellValue("Exportado em " + now.format(formatter)); + headerRow = sheet.createRow(3); //Empty row + + // Set metadata + // Access the POI document properties + POIXMLProperties props = workbook.getProperties(); + + // Core properties + POIXMLProperties.CoreProperties coreProps = props.getCoreProperties(); + coreProps.setCreator("Resilient framework"); + coreProps.setTitle(pageTitle); + coreProps.setSubjectProperty(pageTitle); + coreProps.setDescription("Exported from InNOVA App."); + + // Extended properties + POIXMLProperties.ExtendedProperties extProps = props.getExtendedProperties(); + extProps.getUnderlyingProperties().setCompany("powerd by Resilient, @OGuerreiro.com"); + //extProps.getUnderlyingProperties().setManager("Jane Manager"); + + // Build UO header names + Map UOs = new HashMap(); // Maps a key to a UO code. + for (String key : mapValues.keySet()) { + String code = this.organizationRepository.getReferenceById(Long.valueOf(key)).getCode(); + UOs.put(key, code); + } + + // Header row + c = 0; + headerRow = sheet.createRow(4); + sheet.setColumnWidth(c, 50 * 256); // 50 characters wide. The unit of width is 1/256 of a char. + headerRow.createCell(c++).setCellValue(""); + for (String code : UOs.values()) { + headerRow.createCell(c++).setCellValue(code); + } + + // Get data with a stream (query cursor) + this.recursiveExport(UOs, sheet, organizationId, dashboardComponentId, periodversionId, ""); + + workbook.write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to generate Excel file", e); + } + } + + private void recursiveExport(Map UOs, Sheet sheet, Long organizationId, Long dashboardComponentId, + Long periodversionId, String ident) { + Map> mapValues = this.buildDashboardComponentView(organizationId, + dashboardComponentId, periodversionId); + + // Break recursivity + if (mapValues == null || mapValues.isEmpty() || mapValues.values() == null || mapValues.values().isEmpty()) + return; + + int listSize = mapValues.values().stream().filter(Objects::nonNull).findFirst().orElse( + Collections.emptyList()).size(); + if (listSize == 0) { + return; + } + + for (int i = 0; i < listSize; i++) { + // Create row for this entry + int cc = 0; + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + + // Get detail name from the first entry. They are all the same + DashboardComponentDetail referenceDashboardComponentDetail = mapValues.values().stream().findFirst().orElse( + Collections.emptyList()).get(i).getDashboardComponentDetail(); + String detailName = referenceDashboardComponentDetail.getName(); + row.createCell(cc++).setCellValue(ident + detailName); + + // Write all values (MAX 2, for now) + for (Entry UO : UOs.entrySet()) { + //Extract value from mapValues + BigDecimal value = mapValues.get(UO.getKey()).get(i).getValue(); + row.createCell(cc++).setCellValue(value.setScale(8).toPlainString()); + } + + // Drill down + if (referenceDashboardComponentDetail.getTargetDashboardComponent() != null) { + Long targetDashboardComponentId = referenceDashboardComponentDetail.getTargetDashboardComponent().getId(); + this.recursiveExport(UOs, sheet, organizationId, targetDashboardComponentId, periodversionId, ident + " "); + } + + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/DashboardService.java b/src/main/java/com/oguerreiro/resilient/service/DashboardService.java new file mode 100644 index 0000000..ee1a276 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/DashboardService.java @@ -0,0 +1,38 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Dashboard; +import com.oguerreiro.resilient.repository.DashboardRepository; +import com.oguerreiro.resilient.service.dto.DashboardDTO; +import com.oguerreiro.resilient.service.mapper.DashboardMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.DashboardComponent}. + */ +@Service +@Transactional +public class DashboardService extends AbstractResilientService { + + private final DashboardMapper dashboardMapper; + private final DashboardRepository dashboardRepository; + + public DashboardService(DashboardRepository dashboardRepository, DashboardMapper dashboardMapper) { + super(Dashboard.class, dashboardRepository, dashboardMapper); + + this.dashboardMapper = dashboardMapper; + this.dashboardRepository = dashboardRepository; + } + + @Transactional(readOnly = true) + public List findAllInternal() { + log.debug("Request to get all active Dashboard's."); + return this.dashboardRepository.findAllInternal().stream().map(dashboardMapper::toDto).collect( + Collectors.toCollection(LinkedList::new)); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/DocumentService.java b/src/main/java/com/oguerreiro/resilient/service/DocumentService.java new file mode 100644 index 0000000..e1ba4cc --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/DocumentService.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Document; +import com.oguerreiro.resilient.repository.DocumentRepository; +import com.oguerreiro.resilient.service.dto.DocumentDTO; +import com.oguerreiro.resilient.service.mapper.DocumentMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.Document}. + */ +@Service +@Transactional +public class DocumentService extends AbstractResilientService { + + public DocumentService(DocumentRepository documentRepository, DocumentMapper documentMapper) { + super(Document.class, documentRepository, documentMapper); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/EmailAlreadyUsedException.java b/src/main/java/com/oguerreiro/resilient/service/EmailAlreadyUsedException.java new file mode 100644 index 0000000..e844139 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/EmailAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.service; + +public class EmailAlreadyUsedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public EmailAlreadyUsedException() { + super("Email is already in use!"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/InputDataService.java b/src/main/java/com/oguerreiro/resilient/service/InputDataService.java new file mode 100644 index 0000000..8be4613 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/InputDataService.java @@ -0,0 +1,386 @@ +package com.oguerreiro.resilient.service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.InventoryData; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.error.VariableUnitConverterMoreThanOneFoundException; +import com.oguerreiro.resilient.error.VariableUnitConverterNotFoundException; +import com.oguerreiro.resilient.error.VariableUnitConverterWithoutFormulaException; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.repository.InputDataUploadRepository; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.PeriodRepository; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.repository.VariableRepository; +import com.oguerreiro.resilient.service.dto.InputDataDTO; +import com.oguerreiro.resilient.service.dto.InventoryDataDTO; +import com.oguerreiro.resilient.service.mapper.InputDataMapper; +import com.oguerreiro.resilient.service.mapper.InventoryDataMapper; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; + +/** + * Service Implementation for managing + * {@link com.oguerreiro.resilient.domain.InputData}. + */ +@Service +@Transactional +public class InputDataService { + + private final Logger log = LoggerFactory.getLogger(InputDataService.class); + + private final InputDataRepository inputDataRepository; + private final InputDataUploadRepository inputDataUploadRepository; + private final VariableRepository variableRepository; + private final PeriodRepository periodRepository; + private final UnitRepository unitRepository; + private final OrganizationRepository organizationRepository; + private final OrganizationService organizationService; + private final UnitConverterService unitConverterService; + + private final InputDataMapper inputDataMapper; + private final InventoryDataMapper inventoryDataMapper; + + public InputDataService(InputDataRepository inputDataRepository, InputDataMapper inputDataMapper, + EmissionFactorRepository emissionFactorRepository, VariableRepository variableRepository, + PeriodRepository periodRepository, UnitRepository unitRepository, OrganizationRepository organizationRepository, + InputDataUploadRepository inputDataUploadRepository, UnitConverterService unitConverterService, + OrganizationService organizationService, InventoryDataMapper inventoryDataMapper) { + this.inputDataRepository = inputDataRepository; + this.inputDataMapper = inputDataMapper; + this.variableRepository = variableRepository; + this.periodRepository = periodRepository; + this.unitRepository = unitRepository; + this.organizationRepository = organizationRepository; + this.inputDataUploadRepository = inputDataUploadRepository; + this.unitConverterService = unitConverterService; + this.organizationService = organizationService; + this.inventoryDataMapper = inventoryDataMapper; + } + + /** + * Save a inputData. + * + * @param inputDataDTO the entity to save. + * @return the persisted entity. + */ + public InputDataDTO save(InputDataDTO inputDataDTO) { + log.debug("Request to save InputData : {}", inputDataDTO); + InputData inputData = this.hydrateEntity(inputDataDTO); + + // Evaluate unit convertion + try { + this.evaluateUnitConversion(inputData); + } catch (VariableUnitConverterNotFoundException | VariableUnitConverterWithoutFormulaException + | VariableUnitConverterMoreThanOneFoundException e) { + throw new BadRequestAlertException("Error converting value", "inputData", "idnull"); + } + + inputData = inputDataRepository.save(inputData); + return inputDataMapper.toDto(inputData); + } + + /** + * Update a inputData. + * + * @param inputDataDTO the entity to save. + * @return the persisted entity. + */ + public InputDataDTO update(InputDataDTO inputDataDTO) { + log.debug("Request to update InputData : {}", inputDataDTO); + InputData inputData = this.hydrateEntity(inputDataDTO); + + // Evaluate unit convertion + + try { + this.evaluateUnitConversion(inputData); + } catch (VariableUnitConverterNotFoundException | VariableUnitConverterWithoutFormulaException + | VariableUnitConverterMoreThanOneFoundException e) { + throw new BadRequestAlertException("Error converting value", "inputData", "idnull"); + } + + inputData = inputDataRepository.save(inputData); + return inputDataMapper.toDto(inputData); + } + + /** + * Partially update a inputData. + * + * @param inputDataDTO the entity to update partially. + * @return the persisted entity. + */ + public Optional partialUpdate(InputDataDTO inputDataDTO) { + log.debug("Request to partially update InputData : {}", inputDataDTO); + + return inputDataRepository.findById(inputDataDTO.getId()).map(existingInputData -> { + inputDataMapper.partialUpdate(existingInputData, inputDataDTO); + + return existingInputData; + }).map(inputDataRepository::save).map(inputDataMapper::toDto); + } + + /** + * Get all the inputData. + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAll() { + log.debug("Request to get all InputData"); + return inputDataRepository.findAll().stream().map(inputDataMapper::toDto).collect( + Collectors.toCollection(LinkedList::new)); + } + + /** + * Get all the inputData with eager load of many-to-many relationships. + * + * @return the list of entities. + */ + public Page findAllWithEagerRelationships(Pageable pageable) { + return inputDataRepository.findAllWithEagerRelationships(pageable).map(inputDataMapper::toDto); + } + + /** + * Get one inputData by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOne(Long id) { + log.debug("Request to get InputData : {}", id); + return inputDataRepository.findOneWithEagerRelationships(id).map(inputDataMapper::toDto); + } + + /** + * Delete the inputData by id. + * + * @param id the id of the entity. + */ + public void delete(Long id) { + log.debug("Request to delete InputData : {}", id); + inputDataRepository.deleteById(id); + } + + public void deleteAllInputDataWithSource(InputDataUpload inputDataUpload) { + log.debug("Request to deleteAllInputDataWithSource InputData : {}", inputDataUpload); + inputDataRepository.deleteAllInputDataWithSource(inputDataUpload); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteAllInputDataFromDatafileInNewTransaction(Organization organization, Period period) { + log.debug("Request to deleteAllInputDataFromDatafile InputData : {}", organization, period); + inputDataRepository.deleteAllInputDataFromDatafile(organization, period); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteAllInputDataFromDatafileInNewTransaction(Organization organization, Period period, + UploadType uploadType) { + log.debug("Request to deleteAllInputDataFromDatafile InputData : {}", organization, period, uploadType); + inputDataRepository.deleteAllInputDataFromDatafile(organization, period, uploadType); + } + + @Transactional(readOnly = true) + public byte[] export(Long periodId) { + //Build a Workbook + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("Export"); + + // Header row + int c = 0; + Row headerRow = sheet.createRow(0); + headerRow.createCell(c++).setCellValue("Periodo"); + headerRow.createCell(c++).setCellValue("UO"); + headerRow.createCell(c++).setCellValue("Variável"); + headerRow.createCell(c++).setCellValue(""); + headerRow.createCell(c++).setCellValue("Valor"); + headerRow.createCell(c++).setCellValue("Unidade"); + headerRow.createCell(c++).setCellValue("Classe"); + headerRow.createCell(c++).setCellValue(""); + headerRow.createCell(c++).setCellValue("Fonte"); + headerRow.createCell(c++).setCellValue("Notas"); + + // Get data with a stream (query cursor) + try (Stream stream = this.inputDataRepository.streamAll(periodId)) { + // Loop data + stream.forEach(inputData -> { + int cc = 0; + // Write row + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + row.createCell(cc++).setCellValue(inputData.getPeriod().getName()); + row.createCell(cc++).setCellValue(inputData.getOwner().getCode()); + row.createCell(cc++).setCellValue(inputData.getVariable().getCode()); + row.createCell(cc++).setCellValue(inputData.getVariable().getName()); + row.createCell(cc++).setCellValue( + inputData.getImputedValue() != null ? inputData.getImputedValue().setScale(8).toPlainString() : ""); + row.createCell(cc++).setCellValue(inputData.getUnit().getSymbol()); + row.createCell(cc++).setCellValue(inputData.getVariableClassCode()); + row.createCell(cc++).setCellValue(inputData.getVariableClassName()); + row.createCell(cc++).setCellValue(inputData.getDataSource()); + row.createCell(cc++).setCellValue(inputData.getDataComments()); + }); + } + + workbook.write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to generate Excel file", e); + } + } + + @Transactional(readOnly = true) + public byte[] exportInventory(Long periodId, Long ownerId, Long scopeId, Long categoryId) { + Period period = this.periodRepository.getReferenceById(periodId); + Organization owner = this.organizationRepository.getReferenceById(ownerId); + + //Build a Workbook + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("Export"); + + // Header row + int c = 0; + Row headerRow = sheet.createRow(0); + headerRow.createCell(c++).setCellValue("Periodo"); + headerRow.createCell(c++).setCellValue("UO"); + headerRow.createCell(c++).setCellValue("Variável"); + headerRow.createCell(c++).setCellValue(""); + headerRow.createCell(c++).setCellValue("Classe"); + headerRow.createCell(c++).setCellValue(""); + headerRow.createCell(c++).setCellValue("Valor"); + headerRow.createCell(c++).setCellValue("Unidade"); + + // Get data with a stream (query cursor) + try (Stream stream = this.streamAllInventory(periodId, ownerId, scopeId, categoryId)) { + // Loop data + stream.forEach(inventoryData -> { + int cc = 0; + // Write row + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + row.createCell(cc++).setCellValue(period.getName()); + row.createCell(cc++).setCellValue(owner.getCode()); + row.createCell(cc++).setCellValue(inventoryData.getVariable().getCode()); + row.createCell(cc++).setCellValue(inventoryData.getVariable().getName()); + row.createCell(cc++).setCellValue(inventoryData.getVariableClassCode()); + row.createCell(cc++).setCellValue(inventoryData.getVariableClassName()); + row.createCell(cc++).setCellValue(inventoryData.getValue().setScale(8).toPlainString()); + row.createCell(cc++).setCellValue(inventoryData.getUnit().getSymbol()); + }); + } + + workbook.write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to generate Excel file", e); + } + } + + /** + * Used ONLY when the Entity is received as DTO. In this case, the relations must be correctly hydrated, because DTO + * only have ID; Version; Name (or some other functional info) + * + * @param inputData + */ + private InputData hydrateEntity(InputDataDTO inputDataDTO) { + // Unmarshall the entity + InputData inputData = inputDataMapper.toEntity(inputDataDTO); + + // Hydrate relations + if (inputData.getVariable() != null) + inputData.setVariable( + variableRepository.findByIdAndVersion(inputData.getVariable().getId(), inputData.getVariable().getVersion())); + if (inputData.getPeriod() != null) + inputData.setPeriod( + periodRepository.findByIdAndVersion(inputData.getPeriod().getId(), inputData.getPeriod().getVersion())); + if (inputData.getOwner() != null) + inputData.setOwner( + organizationRepository.findByIdAndVersion(inputData.getOwner().getId(), inputData.getOwner().getVersion())); + if (inputData.getSourceInputData() != null) + inputData.setSourceInputData(inputDataRepository.findByIdAndVersion(inputData.getSourceInputData().getId(), + inputData.getSourceInputData().getVersion())); + if (inputData.getSourceInputDataUpload() != null) + inputData.setSourceInputDataUpload(inputDataUploadRepository.findByIdAndVersion( + inputData.getSourceInputDataUpload().getId(), inputData.getSourceInputDataUpload().getVersion())); + if (inputData.getSourceUnit() != null) + inputData.setSourceUnit( + unitRepository.findByIdAndVersion(inputData.getSourceUnit().getId(), inputData.getSourceUnit().getVersion())); + if (inputData.getUnit() != null) + inputData.setUnit( + unitRepository.findByIdAndVersion(inputData.getUnit().getId(), inputData.getUnit().getVersion())); + + return inputData; + } + + private void evaluateUnitConversion(InputData inputData) throws VariableUnitConverterNotFoundException, + VariableUnitConverterWithoutFormulaException, VariableUnitConverterMoreThanOneFoundException { + // STRING type doesn't need convertion + if (inputData.getSourceUnit().getUnitType().getValueType().equals(UnitValueType.STRING)) + return; + + BigDecimal convertedValue = this.unitConverterService.convertIfNeeded(inputData.getPeriod(), + inputData.getVariable(), inputData.getSourceUnit(), inputData.getSourceValue(), + inputData.getVariableClassCode()); + + inputData.setVariableValue(convertedValue); + inputData.setImputedValue(convertedValue); + } + + public List searchInventory(Long periodId, Long ownerId, Long scopeId, Long categoryId) { + List inventoryData = streamAllInventory(periodId, ownerId, scopeId, categoryId).collect( + Collectors.toList()); + + List entityList = inventoryDataMapper.toDto(inventoryData); + return entityList; + } + + private Stream streamAllInventory(Long periodId, Long ownerId, Long scopeId, Long categoryId) { + boolean hideForMainOrg = false; + + // Must convert the ownerId into a list of descendant Orgs + List orgIds = new ArrayList(); + if (ownerId != null) { + // Always add it self + orgIds.add(ownerId); + + // And also his children + List orgs = organizationService.getAllChildrenOf(ownerId, OrganizationNature.ORGANIZATION); + orgs.stream().forEach(org -> orgIds.add(org.getId())); + + //Set flag for main org + Organization owner = organizationRepository.getReferenceById(ownerId); + if (owner.getParent() == null) { + // Its top ORG, define it as the main org + hideForMainOrg = true; + } + } + + return inputDataRepository.streamAllInventory(periodId, orgIds.isEmpty() ? null : orgIds, scopeId, categoryId, + hideForMainOrg); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/InputDataUploadLogService.java b/src/main/java/com/oguerreiro/resilient/service/InputDataUploadLogService.java new file mode 100644 index 0000000..75c2bb3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/InputDataUploadLogService.java @@ -0,0 +1,56 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.InputDataUploadLog; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.service.dto.InputDataUploadLogDTO; +import com.oguerreiro.resilient.service.mapper.InputDataUploadLogMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.InputDataUploadLog}. + */ +@Service +@Transactional +public class InputDataUploadLogService + extends AbstractResilientService { + + private final InputDataUploadLogRepository inputDataUploadLogRepository; + + private final InputDataUploadLogMapper inputDataUploadLogMapper; + + public InputDataUploadLogService(InputDataUploadLogRepository inputDataUploadLogRepository, + InputDataUploadLogMapper inputDataUploadLogMapper) { + super(InputDataUploadLog.class, inputDataUploadLogRepository, inputDataUploadLogMapper); + this.inputDataUploadLogRepository = inputDataUploadLogRepository; + this.inputDataUploadLogMapper = inputDataUploadLogMapper; + } + + /** + * Get all the inputDataUploadLogs by owner. + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAllByOwner(Long id) { + log.debug("Request to get all InputDataUploadLogs by owner : {}", id); + return inputDataUploadLogRepository.findAllByInputDataUploadId(id).stream().map( + inputDataUploadLogMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteAllByInputDataUploadWithNewTransaction(InputDataUpload inputDataUpload) { + this.deleteAllByInputDataUpload(inputDataUpload); + } + + public void deleteAllByInputDataUpload(InputDataUpload inputDataUpload) { + inputDataUploadLogRepository.deleteAllByInputDataUpload(inputDataUpload); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/InputDataUploadService.java b/src/main/java/com/oguerreiro/resilient/service/InputDataUploadService.java new file mode 100644 index 0000000..0d5d655 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/InputDataUploadService.java @@ -0,0 +1,139 @@ +package com.oguerreiro.resilient.service; + +import java.time.Instant; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.activities.inputDataImporter.POIFileImporterActivity; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.domain.enumeration.UploadStatus; +import com.oguerreiro.resilient.repository.InputDataUploadRepository; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.service.dto.InputDataUploadDTO; +import com.oguerreiro.resilient.service.interceptors.BeforeDeleteServiceInterceptor; +import com.oguerreiro.resilient.service.interceptors.BeforeSaveServiceInterceptor; +import com.oguerreiro.resilient.service.interceptors.BeforeUpdateServiceInterceptor; +import com.oguerreiro.resilient.service.mapper.InputDataUploadMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.InputDataUpload}. + */ +@Service +@Transactional +public class InputDataUploadService extends AbstractResilientService + implements BeforeSaveServiceInterceptor, + BeforeDeleteServiceInterceptor, + BeforeUpdateServiceInterceptor { + + private final InputDataUploadRepository inputDataUploadRepository; + private final POIFileImporterActivity poiFileImporterActivity; + private final InputDataService inputDataService; + private final OrganizationService organizationService; + private final InputDataUploadLogService inputDataUploadLogService; + private final InputDataUploadMapper inputDataUploadMapper; + + public InputDataUploadService(InputDataUploadRepository inputDataUploadRepository, + InputDataUploadMapper inputDataUploadMapper, POIFileImporterActivity poiFileImporterActivity, + InputDataService inputDataService, InputDataUploadLogService inputDataUploadLogService, + OrganizationService organizationService) { + super(InputDataUpload.class, inputDataUploadRepository, inputDataUploadMapper); + this.inputDataUploadRepository = inputDataUploadRepository; + this.poiFileImporterActivity = poiFileImporterActivity; + this.inputDataService = inputDataService; + this.inputDataUploadLogService = inputDataUploadLogService; + this.inputDataUploadMapper = inputDataUploadMapper; + this.organizationService = organizationService; + } + + @Transactional() + public void doLoad(Long id) { + log.debug("Request to doload InputDataUpload : {}", id); + + InputDataUpload inputDataUpload = inputDataUploadRepository.getReferenceById(id); + + // Validate state. Only if UPLOADED + if (!inputDataUpload.getState().equals(UploadStatus.UPLOADED)) { + //throw exception. + } + + // Import the variable data + this.poiFileImporterActivity.doActivity(inputDataUpload.getActivityDomainClass(), inputDataUpload.getId()); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void updateStateWithNewTransaction(InputDataUpload inputDataUpload, UploadStatus newUploadStatus) { + log.debug("Request to delete updateStateinNewTransaction : {}", inputDataUpload, newUploadStatus); + + inputDataUpload = inputDataUploadRepository.getReferenceById(inputDataUpload.getId()); + inputDataUpload.setState(newUploadStatus); + + inputDataUploadRepository.save(inputDataUpload); + } + + @Transactional(readOnly = true) + public List findAllOwned(List organizations) { + log.debug("Request to findAllOwned " + this.getClass().getSimpleName() + "'s."); + return inputDataUploadRepository.findByOwnerIn(organizations).stream().map( + this.inputDataUploadMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + } + + private void validateUserAccessToOwner(InputDataUpload inputDataUpload) { + Organization userParent = SecurityUtils.getCurrentUserParentOrganization(); + if (userParent.equals(inputDataUpload.getOwner())) { + return; //User has access, just exit + } + + // Try the children + userParent = super.getEntityManager().merge(userParent); //Attach + Boolean haveAccess = this.organizationService.getAllChildrenOf(userParent, + OrganizationNature.ORGANIZATION).stream().anyMatch((org) -> (org.equals(inputDataUpload.getOwner()))); + + if (!haveAccess) { + throw new RuntimeException( + "User doesn't have access to the Organization : " + inputDataUpload.getOwner().getCode()); + } + } + + /* ServiceInterceptor's implementation */ + @Override + public void beforeDelete(Long id) { + // Load + InputDataUpload inputDataUpload = inputDataUploadRepository.getReferenceById(id); + + this.validateUserAccessToOwner(inputDataUpload); + + // Delete any previous InputData inserted by this InputDataUpload file. + this.inputDataService.deleteAllInputDataWithSource(inputDataUpload); + + // Clear any associated InputDataUploadLogs. + this.inputDataUploadLogService.deleteAllByInputDataUpload(inputDataUpload); + } + + @Override + public InputDataUpload beforeSave(InputDataUpload domain) { + InputDataUpload inputDataUpload = (InputDataUpload) domain; + + this.validateUserAccessToOwner(domain); + + //Initial setup (defaults) + inputDataUpload.setCreationDate(Instant.now()); + inputDataUpload.setCreationUsername( + SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found"))); + inputDataUpload.setState(UploadStatus.UPLOADED); + + return inputDataUpload; + } + + @Override + public InputDataUpload beforeUpdate(InputDataUpload domain) { + this.validateUserAccessToOwner(domain); + return domain; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/InvalidPasswordException.java b/src/main/java/com/oguerreiro/resilient/service/InvalidPasswordException.java new file mode 100644 index 0000000..d6f9f36 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/InvalidPasswordException.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.service; + +public class InvalidPasswordException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public InvalidPasswordException() { + super("Incorrect password"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/MailService.java b/src/main/java/com/oguerreiro/resilient/service/MailService.java new file mode 100644 index 0000000..cc03fa1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/MailService.java @@ -0,0 +1,120 @@ +package com.oguerreiro.resilient.service; + +import com.oguerreiro.resilient.domain.User; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; +import tech.jhipster.config.JHipsterProperties; + +/** + * Service for sending emails asynchronously. + *

+ * We use the {@link Async} annotation to send emails asynchronously. + */ +@Service +public class MailService { + + private final Logger log = LoggerFactory.getLogger(MailService.class); + + private static final String USER = "user"; + + private static final String BASE_URL = "baseUrl"; + + private final JHipsterProperties jHipsterProperties; + + private final JavaMailSender javaMailSender; + + private final MessageSource messageSource; + + private final SpringTemplateEngine templateEngine; + + public MailService( + JHipsterProperties jHipsterProperties, + JavaMailSender javaMailSender, + MessageSource messageSource, + SpringTemplateEngine templateEngine + ) { + this.jHipsterProperties = jHipsterProperties; + this.javaMailSender = javaMailSender; + this.messageSource = messageSource; + this.templateEngine = templateEngine; + } + + @Async + public void sendEmail(String to, String subject, String content, boolean isMultipart, boolean isHtml) { + this.sendEmailSync(to, subject, content, isMultipart, isHtml); + } + + private void sendEmailSync(String to, String subject, String content, boolean isMultipart, boolean isHtml) { + log.debug( + "Send email[multipart '{}' and html '{}'] to '{}' with subject '{}' and content={}", + isMultipart, + isHtml, + to, + subject, + content + ); + + // Prepare message using a Spring helper + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper message = new MimeMessageHelper(mimeMessage, isMultipart, StandardCharsets.UTF_8.name()); + message.setTo(to); + message.setFrom(jHipsterProperties.getMail().getFrom()); + message.setSubject(subject); + message.setText(content, isHtml); + javaMailSender.send(mimeMessage); + log.debug("Sent email to User '{}'", to); + } catch (MailException | MessagingException e) { + log.warn("Email could not be sent to user '{}'", to, e); + } + } + + @Async + public void sendEmailFromTemplate(User user, String templateName, String titleKey) { + this.sendEmailFromTemplateSync(user, templateName, titleKey); + } + + private void sendEmailFromTemplateSync(User user, String templateName, String titleKey) { + if (user.getEmail() == null) { + log.debug("Email doesn't exist for user '{}'", user.getLogin()); + return; + } + Locale locale = Locale.forLanguageTag(user.getLangKey()); + Context context = new Context(locale); + context.setVariable(USER, user); + context.setVariable(BASE_URL, jHipsterProperties.getMail().getBaseUrl()); + String content = templateEngine.process(templateName, context); + String subject = messageSource.getMessage(titleKey, null, locale); + this.sendEmailSync(user.getEmail(), subject, content, false, true); + } + + @Async + public void sendActivationEmail(User user) { + log.debug("Sending activation email to '{}'", user.getEmail()); + this.sendEmailFromTemplateSync(user, "mail/activationEmail", "email.activation.title"); + } + + @Async + public void sendCreationEmail(User user) { + log.debug("Sending creation email to '{}'", user.getEmail()); + this.sendEmailFromTemplateSync(user, "mail/creationEmail", "email.activation.title"); + } + + @Async + public void sendPasswordResetMail(User user) { + log.debug("Sending password reset email to '{}'", user.getEmail()); + this.sendEmailFromTemplateSync(user, "mail/passwordResetEmail", "email.reset.title"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/MetadataPropertyService.java b/src/main/java/com/oguerreiro/resilient/service/MetadataPropertyService.java new file mode 100644 index 0000000..5110261 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/MetadataPropertyService.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.MetadataProperty; +import com.oguerreiro.resilient.repository.MetadataPropertyRepository; +import com.oguerreiro.resilient.service.dto.MetadataPropertyDTO; +import com.oguerreiro.resilient.service.mapper.MetadataPropertyMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.MetadataProperty}. + */ +@Service +@Transactional +public class MetadataPropertyService extends AbstractResilientService { + + public MetadataPropertyService(MetadataPropertyRepository metadataPropertyRepository, + MetadataPropertyMapper metadataPropertyMapper) { + super(MetadataProperty.class, metadataPropertyRepository, metadataPropertyMapper); + + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/MetadataValueService.java b/src/main/java/com/oguerreiro/resilient/service/MetadataValueService.java new file mode 100644 index 0000000..b600a1a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/MetadataValueService.java @@ -0,0 +1,23 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.MetadataValue; +import com.oguerreiro.resilient.repository.MetadataValueRepository; +import com.oguerreiro.resilient.service.dto.MetadataValueDTO; +import com.oguerreiro.resilient.service.mapper.MetadataValueMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.MetadataValue}. + */ +@Service +@Transactional +public class MetadataValueService extends AbstractResilientService { + + public MetadataValueService(MetadataValueRepository metadataValueRepository, + MetadataValueMapper metadataValueMapper) { + super(MetadataValue.class, metadataValueRepository, metadataValueMapper); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/OrganizationService.java b/src/main/java/com/oguerreiro/resilient/service/OrganizationService.java new file mode 100644 index 0000000..a5df694 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/OrganizationService.java @@ -0,0 +1,156 @@ +package com.oguerreiro.resilient.service; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.mapper.OrganizationMapper; + +import jakarta.persistence.EntityManager; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.Organization}. + */ +@Service +@Transactional +public class OrganizationService extends AbstractResilientService { + + private final OrganizationRepository organizationRepository; + private final OrganizationMapper organizationMapper; + private final EntityManager entityManager; + + public OrganizationService(OrganizationRepository organizationRepository, OrganizationMapper organizationMapper, + EntityManager entityManager) { + super(Organization.class, organizationRepository, organizationMapper); + + this.organizationRepository = organizationRepository; + this.organizationMapper = organizationMapper; + this.entityManager = entityManager; + } + + /** + * First, searches for the parent of the person, with provided nature. + * Then, searches all children of parent, with the same nature. + * + * @param person + * @param nature + * @return + */ + public List findPersonParentAndChildrenOf(Organization person, OrganizationNature nature) { + Organization myPerson = organizationRepository.getById(person.getId()); + + List all = new ArrayList(); + Organization parent = this.findNearestParentOf(myPerson, nature); + all.add(parent); + + all.addAll(this.getAllChildrenOf(parent, nature)); + List allFinal = all.stream().map(organizationMapper::toDto).collect( + Collectors.toCollection(LinkedList::new)); + + return allFinal; + } + + public List getOrganizationsForUserName(User user) { + Organization person = organizationRepository.findOneByUser(user).orElseThrow(); + + List all = new ArrayList(); + Organization parent = this.findNearestParentOf(person, OrganizationNature.ORGANIZATION); + all.add(parent); + + all.addAll(this.getAllChildrenOf(parent, OrganizationNature.ORGANIZATION)); + + return all; + } + + public Organization findNearestParentOf(Organization child, OrganizationNature nature) { + Organization parent = child.getParent(); + if (parent == null) { + // Not found + return null; + } else if (parent.getOrganizationType().getNature().equals(nature)) { + entityManager.refresh(parent); // Refresh parent to reload children collection + return parent; + } else { + return findNearestParentOf(parent, nature); + } + } + + public List getAllChildrenOf(Long parentId, OrganizationNature nature) { + Organization parent = this.getRepository().getReferenceById(parentId); + + return this.getAllChildrenOf(parent, nature); + } + + public List getAllChildrenOf(Organization parent, OrganizationNature nature) { + List children = new LinkedList(); + + for (Organization child : parent.getChildren()) { + if (child.getOrganizationType().getNature().equals(nature)) { + children.add(child); + children.addAll(this.getAllChildrenOf(child, nature)); + } + } + + return children; + } + + /** + * + * @param nature, the {@link OrganizationNature}, if left {@code null} ALL {@link Organization Organization's} + * will be retrieved + * @param forInput, boolean value that indicates you only want {@link Organization Organization's} that allow + * inventory input. If left null, this restriction is ignored + * @param forOutput, boolean value that indicates you only want {@link Organization Organization's} that allow + * inventory output. If left null, this restriction is ignored + * @return + */ + @Transactional(readOnly = true) + public List findAllByNature(OrganizationNature nature, Boolean forInput, Boolean forOutput) { + log.debug("Request to findAllByNature"); + + //@formatter:off + return organizationRepository.findAllOrganizationByNature(nature, forInput, forOutput) + .stream() + .map(organizationMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } + + @Transactional(readOnly = true) + public List findAllByNatureForUserHierarchy(OrganizationNature nature, Boolean forInput, + Boolean forOutput) { + log.debug("Request to findAllByNatureForUserHierarchy"); + + Organization parentOrganization = SecurityUtils.getCurrentUserParentOrganization(); + parentOrganization = this.entityManager.merge(parentOrganization); + + List orgs = new ArrayList(); + + // Add it self + if ((forInput == null || parentOrganization.getInputInventory() == forInput) + && (forOutput == null || parentOrganization.getOutputInventory() == forOutput)) { + orgs.add(organizationMapper.toDto(parentOrganization)); + } + + // Add all his children + if (parentOrganization != null) { + //@formatter:off + orgs.addAll(this.getAllChildrenOf(parentOrganization, nature).stream() + .filter(org -> (forInput == null || org.getInputInventory()==forInput) && (forOutput == null || org.getOutputInventory()==forOutput)) + .map(organizationMapper::toDto).collect(Collectors.toCollection(LinkedList::new))); + //@formatter:on + } + + return orgs; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/OrganizationTypeExtendedService.java b/src/main/java/com/oguerreiro/resilient/service/OrganizationTypeExtendedService.java new file mode 100644 index 0000000..b6629d4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/OrganizationTypeExtendedService.java @@ -0,0 +1,47 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.repository.OrganizationTypeExtendedRepository; +import com.oguerreiro.resilient.repository.OrganizationTypeRepository; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import com.oguerreiro.resilient.service.mapper.OrganizationTypeMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.OrganizationType}. + */ +@Service +@Transactional +public class OrganizationTypeExtendedService extends OrganizationTypeService { + + private final Logger log = LoggerFactory.getLogger(OrganizationTypeExtendedService.class); + + private final OrganizationTypeExtendedRepository organizationTypeRepository; + private final OrganizationTypeMapper organizationTypeMapper; + + public OrganizationTypeExtendedService(OrganizationTypeExtendedRepository organizationTypeRepository, OrganizationTypeMapper organizationTypeMapper) { + super(organizationTypeRepository, organizationTypeMapper); + + this.organizationTypeRepository = organizationTypeRepository; + this.organizationTypeMapper = organizationTypeMapper; + } + + @Transactional(readOnly = true) + public List findByNatureIn(List natures) { + log.debug("Request to get OrganizationTypes restricted by OrganizationNature's"); + return organizationTypeRepository + .findByNatureIn(natures) + .stream() + .map(organizationTypeMapper::toDto) + .collect(Collectors.toCollection(LinkedList::new)); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/OrganizationTypeService.java b/src/main/java/com/oguerreiro/resilient/service/OrganizationTypeService.java new file mode 100644 index 0000000..3bd85d9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/OrganizationTypeService.java @@ -0,0 +1,23 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.OrganizationType; +import com.oguerreiro.resilient.repository.OrganizationTypeRepository; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import com.oguerreiro.resilient.service.mapper.OrganizationTypeMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.OrganizationType}. + */ +@Service +@Transactional +public class OrganizationTypeService extends AbstractResilientService { + + public OrganizationTypeService(OrganizationTypeRepository organizationTypeRepository, + OrganizationTypeMapper organizationTypeMapper) { + super(OrganizationType.class, organizationTypeRepository, organizationTypeMapper); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/OutputDataService.java b/src/main/java/com/oguerreiro/resilient/service/OutputDataService.java new file mode 100644 index 0000000..ceee334 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/OutputDataService.java @@ -0,0 +1,93 @@ +package com.oguerreiro.resilient.service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.stream.Stream; + +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.repository.OutputDataRepository; +import com.oguerreiro.resilient.service.dto.OutputDataDTO; +import com.oguerreiro.resilient.service.mapper.OutputDataMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.OutputData}. + */ +@Service +@Transactional +public class OutputDataService extends AbstractResilientService { + + private final OutputDataRepository outputDataRepository; + + private final OutputDataMapper outputDataMapper; + + public OutputDataService(OutputDataRepository outputDataRepository, OutputDataMapper outputDataMapper) { + super(OutputData.class, outputDataRepository, outputDataMapper); + this.outputDataRepository = outputDataRepository; + this.outputDataMapper = outputDataMapper; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteAllByPeriodVersionInNewTransaction(PeriodVersion periodVersion) { + // Clear logs for InputdataUpload entity + outputDataRepository.deleteAllByPeriodVersion(periodVersion); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveInNewTransaction(OutputData outputData) { + outputDataRepository.save(outputData); + } + + public BigDecimal sumAllOutputsFor(Organization organization, PeriodVersion periodVersion, String variableCode) { + return this.outputDataRepository.sumAllOutputsFor(organization, periodVersion, variableCode); + } + + @Transactional(readOnly = true) + public byte[] export(Long periodVersionId) { + //Build a Workbook + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("Export"); + + // Header row + int c = 0; + Row headerRow = sheet.createRow(0); + headerRow.createCell(c++).setCellValue("Periodo"); + headerRow.createCell(c++).setCellValue("UO"); + headerRow.createCell(c++).setCellValue("Variável"); + headerRow.createCell(c++).setCellValue(""); + headerRow.createCell(c++).setCellValue("Valor"); + headerRow.createCell(c++).setCellValue("Unidade"); + + // Get data with a stream (query cursor) + try (Stream stream = this.outputDataRepository.streamAll(periodVersionId)) { + // Loop data + stream.forEach(outputData -> { + int cc = 0; + // Write row + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + row.createCell(cc++).setCellValue(outputData.getPeriod().getName()); + row.createCell(cc++).setCellValue(outputData.getOwner().getCode()); + row.createCell(cc++).setCellValue(outputData.getVariable().getCode()); + row.createCell(cc++).setCellValue(outputData.getVariable().getName()); + row.createCell(cc++).setCellValue(outputData.getValue().setScale(8).toPlainString()); + row.createCell(cc++).setCellValue(outputData.getBaseUnit().getSymbol()); + }); + } + + workbook.write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to generate Excel file", e); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/PeriodService.java b/src/main/java/com/oguerreiro/resilient/service/PeriodService.java new file mode 100644 index 0000000..8577714 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/PeriodService.java @@ -0,0 +1,158 @@ +package com.oguerreiro.resilient.service; + +import java.time.Instant; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.repository.PeriodRepository; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.service.dto.PeriodDTO; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; +import com.oguerreiro.resilient.service.interceptors.BeforeSaveServiceInterceptor; +import com.oguerreiro.resilient.service.mapper.PeriodMapper; +import com.oguerreiro.resilient.service.mapper.PeriodVersionMapper; + +import jakarta.persistence.EntityNotFoundException; + +/** + * Service Implementation for managing + * {@link com.oguerreiro.resilient.domain.Period}. + */ +@Service +@Transactional +public class PeriodService extends AbstractResilientService + implements BeforeSaveServiceInterceptor { + + private final PeriodRepository periodRepository; + + private final PeriodMapper periodMapper; + private final PeriodVersionMapper periodVersionMapper; + + public PeriodService(PeriodRepository periodRepository, PeriodMapper periodMapper, + PeriodVersionMapper periodVersionMapper) { + super(Period.class, periodRepository, periodMapper); + this.periodRepository = periodRepository; + this.periodMapper = periodMapper; + this.periodVersionMapper = periodVersionMapper; + } + + /* Interface BeforeSaveServiceInterceptor */ + @Override + public Period beforeSave(Period domain) { + Period period = (Period) domain; + + // Initial setup (defaults) + period.setCreationDate(Instant.now()); + period.setCreationUsername( + SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found"))); + period.setState(PeriodStatus.CREATED); + + return period; + } + + /** + * Update a period. + * + * @param periodDTO the entity to save. + * @return the persisted entity. + */ + @Override + public PeriodDTO update(PeriodDTO periodDTO) { + log.debug("Request to update Period : {}", periodDTO); + + Period period = this.updateSafe(periodDTO); + return periodMapper.toDto(period); + } + + public Period updateSafe(PeriodDTO periodDTO) { + log.debug("Request to updateSafe Period : {}", periodDTO); + + Period existingPeriod = periodRepository.findByIdAndVersionWithRelationships(periodDTO.getId(), + periodDTO.getVersion()).orElseThrow(() -> new EntityNotFoundException("Period not found")); + + // Merge Period updatable properties + periodMapper.updatePeriodFromDto(periodDTO, existingPeriod); + + // Merge PeriodVersion's collections, updatable properties + Set updatedPeriodVersions = periodDTO.getPeriodVersions().stream().map(periodVersionDTO -> { + // New entry? Just add it + if (periodVersionDTO.getId() == null) { + PeriodVersion newPeriodVersion = periodMapper.toPeriodVersion(periodVersionDTO); + newPeriodVersion.setCreationDate(null); + newPeriodVersion.setCreationUsername(null); + newPeriodVersion.setState(null); + existingPeriod.addPeriodVersion(newPeriodVersion); + return newPeriodVersion; + } + + // Find the PeriodVersion in the loaded Period.PerdiodVersions + PeriodVersion periodVersion = findExistingPeriodVersion(existingPeriod.getPeriodVersions(), + periodVersionDTO.getId()); + if (periodVersion == null) { + // Not found. Add it... hummm, isn't it strange that it wasn't found??? + throw new RuntimeException( + "This is totally unexpected. Only new PeriodVersion's (without id) can be added. But in this caso, the PeriodVersion exists, but is not found in the persisted Period !!"); + } else { + // Update the existing PeriodVersion + return updatePeriodPeriodVersion(periodVersion, periodVersionDTO); + } + }).collect(Collectors.toSet()); + + // Must use the same collection instance, or Hibernate will throw an exception + // because the collection is already in the session. + existingPeriod.getPeriodVersions().clear(); + existingPeriod.getPeriodVersions().addAll(updatedPeriodVersions); + + return periodRepository.save(existingPeriod); + } + + /** + * Get one period by id, with relations. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOneWithToOneRelationships(Long id) { + log.debug("Request to get Period : {}", id); + return periodRepository.findOneWithEagerRelationships(id).map(periodMapper::toDto); + } + + private PeriodVersion findExistingPeriodVersion(List periodVersions, Long id) { + return periodVersions.stream().filter(item -> item.getId().equals(id)).findFirst().orElse(null); + } + + private PeriodVersion updatePeriodPeriodVersion(PeriodVersion periodVersion, PeriodVersionDTO perdiotVersionDto) { + + periodVersion.setDescription(perdiotVersionDto.getDescription()); + periodVersion.setName(perdiotVersionDto.getName()); + return periodVersion; + } + + @Transactional(readOnly = true) + public List findAllByState(PeriodStatus state) { + log.debug("REST request to get all findAllByStatus : {}", state); + //@formatter:off + return periodRepository.findAllByState(state) + .stream() + .map(periodMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveWithNewTransaction(Period period) { + log.debug("REST request to get all saveWithNewTransaction : {}", period); + + this.periodRepository.save(period); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/PeriodVersionService.java b/src/main/java/com/oguerreiro/resilient/service/PeriodVersionService.java new file mode 100644 index 0000000..8eb6eaf --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/PeriodVersionService.java @@ -0,0 +1,52 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.repository.PeriodVersionRepository; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; +import com.oguerreiro.resilient.service.mapper.PeriodVersionMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.PeriodVersion}. + */ +@Service +@Transactional +public class PeriodVersionService extends AbstractResilientService { + + private PeriodVersionRepository periodVersionRepository; + private PeriodVersionMapper periodVersionMapper; + + public PeriodVersionService(PeriodVersionRepository periodVersionRepository, + PeriodVersionMapper periodVersionMapper) { + super(PeriodVersion.class, periodVersionRepository, periodVersionMapper); + this.periodVersionRepository = periodVersionRepository; + this.periodVersionMapper = periodVersionMapper; + } + + public List findAllByPeriod(Long periodId) { + log.debug("REST request to get all findAllByPeriod Id : {}", periodId); + //@formatter:off + return periodVersionRepository.findAllByPeriod(periodId) + .stream() + .map(periodVersionMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } + + public PeriodVersionDTO findLatestVersionForPeriod(Long periodId) { + log.debug("REST request to get all findLatestVersionForPeriod Id : {}", periodId); + + List endedVersions = periodVersionRepository.findEndedPeriodVersionsForPeriod(periodId); + + if (endedVersions == null || endedVersions.isEmpty()) { + return null; + } + + return periodVersionMapper.toDto(endedVersions.get(0)); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/ResilientLogService.java b/src/main/java/com/oguerreiro/resilient/service/ResilientLogService.java new file mode 100644 index 0000000..e7cb267 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/ResilientLogService.java @@ -0,0 +1,59 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.ResilientLog; +import com.oguerreiro.resilient.repository.ResilientLogRepository; +import com.oguerreiro.resilient.service.dto.ResilientLogDTO; +import com.oguerreiro.resilient.service.mapper.ResilientLogMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.ResilientLog}. + */ +@Service +@Transactional +public class ResilientLogService extends AbstractResilientService { + + private final ResilientLogRepository resilientLogRepository; + + private final ResilientLogMapper resilientLogMapper; + + public ResilientLogService(ResilientLogRepository inputDataUploadLogRepository, + ResilientLogMapper inputDataUploadLogMapper) { + super(ResilientLog.class, inputDataUploadLogRepository, inputDataUploadLogMapper); + this.resilientLogRepository = inputDataUploadLogRepository; + this.resilientLogMapper = inputDataUploadLogMapper; + } + + /** + * Get all the resilientLogs by owner. + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAllByOwnerId(Long ownerId) { + log.debug("Request to get all by findAllByOwnerId : {}", ownerId); + return resilientLogRepository.findAllByOwnerId(ownerId).stream().map(resilientLogMapper::toDto).collect( + Collectors.toCollection(LinkedList::new)); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveWithNewTransaction(ResilientLog log) { + this.resilientLogRepository.save(log); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteAllByOwnerIdWithNewTransaction(Long ownerId) { + this.deleteAllByOwnerId(ownerId); + } + + public void deleteAllByOwnerId(Long ownerId) { + resilientLogRepository.deleteAllByOwnerId(ownerId); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/UnitConverterService.java b/src/main/java/com/oguerreiro/resilient/service/UnitConverterService.java new file mode 100644 index 0000000..08ad0ab --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/UnitConverterService.java @@ -0,0 +1,167 @@ +package com.oguerreiro.resilient.service; + +import java.math.BigDecimal; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.VariableUnits; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; +import com.oguerreiro.resilient.error.ExpressionEvaluationException; +import com.oguerreiro.resilient.error.UnitConverterExpressionEvaluationRuntimeException; +import com.oguerreiro.resilient.error.VariableUnitConverterMoreThanOneFoundException; +import com.oguerreiro.resilient.error.VariableUnitConverterNotFoundException; +import com.oguerreiro.resilient.error.VariableUnitConverterWithoutFormulaException; +import com.oguerreiro.resilient.expression.ExpressionUtils; +import com.oguerreiro.resilient.expression.UnitConverterExpressionContext; +import com.oguerreiro.resilient.expression.UnitConverterExpressionFunctions; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.repository.PeriodRepository; +import com.oguerreiro.resilient.repository.UnitConverterRepository; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.repository.VariableRepository; +import com.oguerreiro.resilient.service.dto.UnitConverterDTO; +import com.oguerreiro.resilient.service.interceptors.BeforeSaveServiceInterceptor; +import com.oguerreiro.resilient.service.interceptors.BeforeUpdateServiceInterceptor; +import com.oguerreiro.resilient.service.mapper.UnitConverterMapper; + +/** + * Service Implementation for managing + * {@link com.oguerreiro.resilient.domain.UnitConverter}. + */ +@Service +@Transactional +public class UnitConverterService extends AbstractResilientService + implements BeforeSaveServiceInterceptor, + BeforeUpdateServiceInterceptor { + + private final UnitConverterRepository unitConverterRepository; + private final UnitConverterMapper unitConverterMapper; + private final EmissionFactorRepository emissionFactorRepository; + private final ApplicationContext applicationContext; + + private final PeriodRepository periodRepository; + private final VariableRepository variableRepository; + private final UnitRepository unitRepository; + + public UnitConverterService(UnitConverterRepository unitConverterRepository, UnitConverterMapper unitConverterMapper, + EmissionFactorRepository emissionFactorRepository, ApplicationContext applicationContext, + PeriodRepository periodRepository, VariableRepository variableRepository, UnitRepository unitRepository) { + super(UnitConverter.class, unitConverterRepository, unitConverterMapper); + + this.unitConverterRepository = unitConverterRepository; + this.unitConverterMapper = unitConverterMapper; + this.emissionFactorRepository = emissionFactorRepository; + this.applicationContext = applicationContext; + + this.periodRepository = periodRepository; + this.variableRepository = variableRepository; + this.unitRepository = unitRepository; + } + + @Override + public UnitConverter beforeSave(UnitConverter domain) { + // Validate expression. Syntax only. + ExpressionUtils.validateExpression(domain.getConvertionFormula(), + UnitConverterExpressionContext.create(emissionFactorRepository)); + + return domain; + } + + @Override + public UnitConverter beforeUpdate(UnitConverter domain) { + // Validate expression. Syntax only. + ExpressionUtils.validateExpression(domain.getConvertionFormula(), + UnitConverterExpressionContext.create(emissionFactorRepository)); + + return domain; + } + + @Transactional(readOnly = true) + public List findAllFromAndTo(Long fromUnitId, Long toUnitId) { + log.debug("Request to get a list of UnitConverters that math {fromUnitId} and {toUnitId}"); + return unitConverterRepository.findAllFromAndTo(fromUnitId, toUnitId).stream().map( + unitConverterMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + } + + public BigDecimal convertIfNeeded(Long periodId, Long variableId, Long sourceUnitId, BigDecimal value, + String variableClassCode) throws VariableUnitConverterNotFoundException, + VariableUnitConverterWithoutFormulaException, VariableUnitConverterMoreThanOneFoundException { + Period period = periodRepository.getReferenceById(periodId); + Variable variable = variableRepository.getReferenceById(variableId); + Unit sourceUnit = unitRepository.getReferenceById(sourceUnitId); + + return convertIfNeeded(period, variable, sourceUnit, value, variableClassCode); + } + + public BigDecimal convertIfNeeded(Period period, Variable variable, Unit sourceUnit, BigDecimal value, + String variableClassCode) throws VariableUnitConverterNotFoundException, + VariableUnitConverterWithoutFormulaException, VariableUnitConverterMoreThanOneFoundException { + if (sourceUnit.getUnitType().getValueType().equals(UnitValueType.STRING)) + throw new RuntimeException("UnitValueType STRING has no convertion."); + + BigDecimal convertedValue = null; + + if (sourceUnit.equals(variable.getBaseUnit())) { + // Its the same. No need to convert value. + convertedValue = value; + } else { + // Conversion is needed. The units are both of the same UniType ? + if (sourceUnit.getUnitType().equals(variable.getBaseUnit().getUnitType())) { + // Same UnitType, the conversion is just a scale conversion + // (sourceValue*CR(target unit)/CR(source unit)) + convertedValue = value.multiply( + variable.getBaseUnit().getConvertionRate().divide(sourceUnit.getConvertionRate())); + } else { + // This is a more complex conversion. Needs a UnitConverter + // First: Is this unit allowed in Variable? + List sourceVariableUnits = variable.getVariableUnits().stream().filter( + vUnit -> vUnit.getUnit().equals(sourceUnit)).toList(); + if (sourceVariableUnits.isEmpty()) { + throw new VariableUnitConverterNotFoundException(variable.getCode(), sourceUnit.getCode(), + variable.getBaseUnit().getCode()); + } else if (sourceVariableUnits.size() > 1) { + throw new VariableUnitConverterMoreThanOneFoundException(variable.getCode(), sourceUnit.getCode(), + variable.getBaseUnit().getCode()); + } + + VariableUnits variableUnit = sourceVariableUnits.get(0); + UnitConverter unitConverter = variableUnit.getUnitConverter(); + + // Unit converter without conversion formula + if (unitConverter.getConvertionFormula() == null || unitConverter.getConvertionFormula().isBlank()) { + throw new VariableUnitConverterWithoutFormulaException(variable.getCode(), sourceUnit.getCode(), + variable.getBaseUnit().getCode()); + } + + // Evaluate formula to convert the value + UnitConverterExpressionFunctions expressionFunctions = new UnitConverterExpressionFunctions(period); + applicationContext.getAutowireCapableBeanFactory().autowireBean(expressionFunctions); //This is CRUCIAL, Without this there will be no dependency injection + + UnitConverterExpressionContext context = UnitConverterExpressionContext.create(emissionFactorRepository, + period.getBeginDate().getYear(), value, sourceUnit, variable.getBaseUnit(), variable, variableClassCode); + context.setExpressionFunctions(expressionFunctions); + + String convertionFormula = unitConverter.getConvertionFormula(); + + try { + Object resultValue = ExpressionUtils.evalExpression(convertionFormula, context); + convertedValue = ExpressionUtils.toBigDecimal(resultValue); + } catch (ExpressionEvaluationException e) { + throw new UnitConverterExpressionEvaluationRuntimeException(variable.getCode(), sourceUnit.getCode(), + variable.getBaseUnit().getCode(), value.toString(), e); + } + } + } + + return convertedValue; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/UnitService.java b/src/main/java/com/oguerreiro/resilient/service/UnitService.java new file mode 100644 index 0000000..72cc34b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/UnitService.java @@ -0,0 +1,65 @@ +package com.oguerreiro.resilient.service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.mapper.UnitMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.Unit}. + */ +@Service +@Transactional +public class UnitService extends AbstractResilientService { + private final UnitRepository unitRepository; + private final UnitMapper unitMapper; + + public UnitService(UnitRepository unitRepository, UnitMapper unitMapper) { + super(Unit.class, unitRepository, unitMapper); + + this.unitRepository = unitRepository; + this.unitMapper = unitMapper; + } + + /** + * Get all the units, restricted by type. + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAllByType(Long unitTypeId) { + log.debug("Request to get all Units"); + return unitRepository.findAllByTypeWithToOneRelationships(unitTypeId).stream().map(unitMapper::toDto).collect( + Collectors.toCollection(LinkedList::new)); + } + + /** + * Get all the units allowed in variable + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAllUnitsAllowedForVariable(Long variableId) { + log.debug("Request to findAllUnitsAllowedForVariable"); + + List units = new ArrayList<>(); + units.addAll(unitRepository.findAllUnitsAllowedForVariable(variableId)); + units.addAll(unitRepository.findBaseUnitForVariable(variableId)); + + // @formatter:off + return units.stream() + .sorted(Comparator.comparing(Unit::getCode)) // Sort by code + .map(unitMapper::toDto) + .collect(Collectors.toList()); + // @formatter:on + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/UnitTypeService.java b/src/main/java/com/oguerreiro/resilient/service/UnitTypeService.java new file mode 100644 index 0000000..61e1390 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/UnitTypeService.java @@ -0,0 +1,21 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.UnitType; +import com.oguerreiro.resilient.repository.UnitTypeRepository; +import com.oguerreiro.resilient.service.dto.UnitTypeDTO; +import com.oguerreiro.resilient.service.mapper.UnitTypeMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.UnitType}. + */ +@Service +@Transactional +public class UnitTypeService extends AbstractResilientService { + + public UnitTypeService(UnitTypeRepository unitTypeRepository, UnitTypeMapper unitTypeMapper) { + super(UnitType.class, unitTypeRepository, unitTypeMapper); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/UserService.java b/src/main/java/com/oguerreiro/resilient/service/UserService.java new file mode 100644 index 0000000..aa6a340 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/UserService.java @@ -0,0 +1,335 @@ +package com.oguerreiro.resilient.service; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.config.Constants; +import com.oguerreiro.resilient.domain.Authority; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.AuthorityRepository; +import com.oguerreiro.resilient.repository.PersistentTokenRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.repository.security.SecurityGroupRepository; +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import com.oguerreiro.resilient.service.dto.UserDTO; + +import tech.jhipster.security.RandomUtil; + +/** + * Service class for managing users. + */ +@Service +@Transactional +public class UserService { + + private final Logger log = LoggerFactory.getLogger(UserService.class); + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + private final PersistentTokenRepository persistentTokenRepository; + + private final AuthorityRepository authorityRepository; + + private final SecurityGroupRepository securityGroupRepository; + + private final CacheManager cacheManager; + + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, + PersistentTokenRepository persistentTokenRepository, AuthorityRepository authorityRepository, + CacheManager cacheManager, SecurityGroupRepository securityGroupRepository) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.persistentTokenRepository = persistentTokenRepository; + this.authorityRepository = authorityRepository; + this.cacheManager = cacheManager; + this.securityGroupRepository = securityGroupRepository; + } + + public Optional activateRegistration(String key) { + log.debug("Activating user for activation key {}", key); + return userRepository.findOneByActivationKey(key).map(user -> { + // activate given user for the registration key. + user.setActivated(true); + user.setActivationKey(null); + this.clearUserCaches(user); + log.debug("Activated user: {}", user); + return user; + }); + } + + public Optional completePasswordReset(String newPassword, String key) { + log.debug("Reset user password for reset key {}", key); + return userRepository.findOneByResetKey(key).filter( + user -> user.getResetDate().isAfter(Instant.now().minus(1, ChronoUnit.DAYS))).map(user -> { + user.setPassword(passwordEncoder.encode(newPassword)); + user.setResetKey(null); + user.setResetDate(null); + this.clearUserCaches(user); + return user; + }); + } + + public Optional requestPasswordReset(String mail) { + return userRepository.findOneByEmailIgnoreCase(mail).filter(User::isActivated).map(user -> { + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(Instant.now()); + this.clearUserCaches(user); + return user; + }); + } + + public User registerUser(AdminUserDTO userDTO, String password) { + userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()).ifPresent(existingUser -> { + boolean removed = removeNonActivatedUser(existingUser); + if (!removed) { + throw new UsernameAlreadyUsedException(); + } + }); + userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()).ifPresent(existingUser -> { + boolean removed = removeNonActivatedUser(existingUser); + if (!removed) { + throw new EmailAlreadyUsedException(); + } + }); + User newUser = new User(); + String encryptedPassword = passwordEncoder.encode(password); + newUser.setLogin(userDTO.getLogin().toLowerCase()); + // new user gets initially a generated password + newUser.setPassword(encryptedPassword); + newUser.setFirstName(userDTO.getFirstName()); + newUser.setLastName(userDTO.getLastName()); + if (userDTO.getEmail() != null) { + newUser.setEmail(userDTO.getEmail().toLowerCase()); + } + newUser.setImageUrl(userDTO.getImageUrl()); + newUser.setLangKey(userDTO.getLangKey()); + // new user is not active + newUser.setActivated(false); + // new user gets registration key + newUser.setActivationKey(RandomUtil.generateActivationKey()); + Set authorities = new HashSet<>(); + authorityRepository.findById(AuthoritiesConstants.USER).ifPresent(authorities::add); + newUser.setAuthorities(authorities); + if (userDTO.getSecurityGroup() != null) { + newUser.setSecurityGroup(securityGroupRepository.getReferenceById(userDTO.getSecurityGroup().getId())); + } + userRepository.save(newUser); + this.clearUserCaches(newUser); + log.debug("Created Information for User: {}", newUser); + return newUser; + } + + private boolean removeNonActivatedUser(User existingUser) { + if (existingUser.isActivated()) { + return false; + } + userRepository.delete(existingUser); + userRepository.flush(); + this.clearUserCaches(existingUser); + return true; + } + + public User createUser(AdminUserDTO userDTO) { + User user = new User(); + user.setLogin(userDTO.getLogin().toLowerCase()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + if (userDTO.getEmail() != null) { + user.setEmail(userDTO.getEmail().toLowerCase()); + } + user.setImageUrl(userDTO.getImageUrl()); + if (userDTO.getLangKey() == null) { + user.setLangKey(Constants.DEFAULT_LANGUAGE); // default language + } else { + user.setLangKey(userDTO.getLangKey()); + } + String encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword()); + user.setPassword(encryptedPassword); + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(Instant.now()); + user.setActivated(true); + if (userDTO.getAuthorities() != null) { + Set authorities = userDTO.getAuthorities().stream().map(authorityRepository::findById).filter( + Optional::isPresent).map(Optional::get).collect(Collectors.toSet()); + user.setAuthorities(authorities); + } + if (userDTO.getSecurityGroup() != null) { + user.setSecurityGroup(securityGroupRepository.getReferenceById(userDTO.getSecurityGroup().getId())); + } + userRepository.save(user); + this.clearUserCaches(user); + log.debug("Created Information for User: {}", user); + return user; + } + + /** + * Update all information for a specific user, and return the modified user. + * + * @param userDTO user to update. + * @return updated user. + */ + public Optional updateUser(AdminUserDTO userDTO) { + return Optional.of(userRepository.findById(userDTO.getId())).filter(Optional::isPresent).map(Optional::get).map( + user -> { + this.clearUserCaches(user); + user.setLogin(userDTO.getLogin().toLowerCase()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + if (userDTO.getEmail() != null) { + user.setEmail(userDTO.getEmail().toLowerCase()); + } + user.setImageUrl(userDTO.getImageUrl()); + user.setActivated(userDTO.isActivated()); + user.setLangKey(userDTO.getLangKey()); + Set managedAuthorities = user.getAuthorities(); + managedAuthorities.clear(); + userDTO.getAuthorities().stream().map(authorityRepository::findById).filter(Optional::isPresent).map( + Optional::get).forEach(managedAuthorities::add); + if (userDTO.getSecurityGroup() != null) { + user.setSecurityGroup(securityGroupRepository.getReferenceById(userDTO.getSecurityGroup().getId())); + } else { + user.setSecurityGroup(null); + } + userRepository.save(user); + this.clearUserCaches(user); + log.debug("Changed Information for User: {}", user); + return user; + }).map(AdminUserDTO::new); + } + + public void deleteUser(String login) { + userRepository.findOneByLogin(login).ifPresent(user -> { + userRepository.delete(user); + this.clearUserCaches(user); + log.debug("Deleted User: {}", user); + }); + } + + /** + * Update basic information (first name, last name, email, language) for the current user. + * + * @param firstName first name of user. + * @param lastName last name of user. + * @param email email id of user. + * @param langKey language key. + * @param imageUrl image URL of user. + */ + public void updateUser(String firstName, String lastName, String email, String langKey, String imageUrl) { + SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneByLogin).ifPresent(user -> { + user.setFirstName(firstName); + user.setLastName(lastName); + if (email != null) { + user.setEmail(email.toLowerCase()); + } + user.setLangKey(langKey); + user.setImageUrl(imageUrl); + userRepository.save(user); + this.clearUserCaches(user); + log.debug("Changed Information for User: {}", user); + }); + } + + @Transactional + public void changePassword(String currentClearTextPassword, String newPassword) { + SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneByLogin).ifPresent(user -> { + String currentEncryptedPassword = user.getPassword(); + if (!passwordEncoder.matches(currentClearTextPassword, currentEncryptedPassword)) { + throw new InvalidPasswordException(); + } + String encryptedPassword = passwordEncoder.encode(newPassword); + user.setPassword(encryptedPassword); + this.clearUserCaches(user); + log.debug("Changed password for User: {}", user); + }); + } + + @Transactional(readOnly = true) + public Page getAllManagedUsers(Pageable pageable) { + return userRepository.findAll(pageable).map(AdminUserDTO::new); + } + + @Transactional(readOnly = true) + public Page getAllPublicUsers(Pageable pageable) { + return userRepository.findAllByIdNotNullAndActivatedIsTrue(pageable).map(UserDTO::new); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthoritiesByLogin(String login) { + return userRepository.findOneWithAuthoritiesByLogin(login); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthorities() { + return SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneWithAuthoritiesByLogin); + } + + /** + * Persistent Token are used for providing automatic authentication, they should be automatically deleted after + * 30 days. + *

+ * This is scheduled to get fired everyday, at midnight. + */ + @Scheduled(cron = "0 0 0 * * ?") + public void removeOldPersistentTokens() { + LocalDate now = LocalDate.now(); + persistentTokenRepository.findByTokenDateBefore(now.minusMonths(1)).forEach(token -> { + log.debug("Deleting token {}", token.getSeries()); + User user = token.getUser(); + user.getPersistentTokens().remove(token); + persistentTokenRepository.delete(token); + }); + } + + /** + * Not activated users should be automatically deleted after 3 days. + *

+ * This is scheduled to get fired everyday, at 01:00 (am). + */ + @Scheduled(cron = "0 0 1 * * ?") + public void removeNotActivatedUsers() { + userRepository.findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore( + Instant.now().minus(3, ChronoUnit.DAYS)).forEach(user -> { + log.debug("Deleting not activated user {}", user.getLogin()); + userRepository.delete(user); + this.clearUserCaches(user); + }); + } + + /** + * Gets a list of all the authorities. + * + * @return a list of all the authorities. + */ + @Transactional(readOnly = true) + public List getAuthorities() { + return authorityRepository.findAll().stream().map(Authority::getName).toList(); + } + + private void clearUserCaches(User user) { + Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE)).evict(user.getLogin()); + if (user.getEmail() != null) { + Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_EMAIL_CACHE)).evict(user.getEmail()); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/UsernameAlreadyUsedException.java b/src/main/java/com/oguerreiro/resilient/service/UsernameAlreadyUsedException.java new file mode 100644 index 0000000..b475a37 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/UsernameAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.service; + +public class UsernameAlreadyUsedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public UsernameAlreadyUsedException() { + super("Login name already used!"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/VariableCategoryService.java b/src/main/java/com/oguerreiro/resilient/service/VariableCategoryService.java new file mode 100644 index 0000000..36ee597 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/VariableCategoryService.java @@ -0,0 +1,62 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.repository.VariableCategoryRepository; +import com.oguerreiro.resilient.service.dto.VariableCategoryDTO; +import com.oguerreiro.resilient.service.mapper.VariableCategoryMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.VariableCategory}. + */ +@Service +@Transactional +public class VariableCategoryService extends AbstractResilientService { + + private final VariableCategoryRepository variableCategoryRepository; + private final VariableCategoryMapper variableCategoryMapper; + + public VariableCategoryService(VariableCategoryRepository variableCategoryRepository, + VariableCategoryMapper variableCategoryMapper) { + super(VariableCategory.class, variableCategoryRepository, variableCategoryMapper); + + this.variableCategoryRepository = variableCategoryRepository; + this.variableCategoryMapper = variableCategoryMapper; + } + + /** + * Get all the Categories, restricted by Scope. With option to filter hiddenForData.
+ * all==false, the resulting List WON'T have VariableCategories.hiddenForData == TRUE + * all==true, the resulting List will have all without restriction + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAllByScope(Long scopeId, Boolean all) { + log.debug("Request to get all VariableCategories"); + + //@formatter:off + return variableCategoryRepository.findAllByScopeWithToOneRelationships(scopeId) + .stream() + .filter(vc -> all || vc.getHiddenForData() == null || !vc.getHiddenForData()) // Filters VariableCategory.hiddenForData + .map(variableCategoryMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } + + @Transactional(readOnly = true) + public List findAllByScopeForFactor(Long scopeId) { + log.debug("Request to get all findAllByScopeForFactor"); + + //@formatter:off + return variableCategoryRepository.findByVariableScopeIdAndHiddenForFactorIsFalseOrHiddenForFactorIsNullOrderByNameAsc(scopeId) + .stream() + .map(variableCategoryMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/VariableClassService.java b/src/main/java/com/oguerreiro/resilient/service/VariableClassService.java new file mode 100644 index 0000000..20d33ef --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/VariableClassService.java @@ -0,0 +1,39 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.VariableClass; +import com.oguerreiro.resilient.repository.VariableClassRepository; +import com.oguerreiro.resilient.service.dto.VariableClassDTO; +import com.oguerreiro.resilient.service.mapper.VariableClassMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.VariableClass}. + */ +@Service +@Transactional +public class VariableClassService extends AbstractResilientService { + private final VariableClassRepository variableClassRepository; + private final VariableClassMapper variableClassMapper; + + public VariableClassService(VariableClassRepository variableClassRepository, + VariableClassMapper variableClassMapper) { + super(VariableClass.class, variableClassRepository, variableClassMapper); + this.variableClassRepository = variableClassRepository; + this.variableClassMapper = variableClassMapper; + } + + public List getAllForVariable(Long variableId) { + log.debug("REST request to get all getAllForVariable Id : {}", variableId); + //@formatter:off + return variableClassRepository.getAllForVariable(variableId) + .stream() + .map(variableClassMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/VariableClassTypeService.java b/src/main/java/com/oguerreiro/resilient/service/VariableClassTypeService.java new file mode 100644 index 0000000..ecf2a1f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/VariableClassTypeService.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.VariableClassType; +import com.oguerreiro.resilient.repository.VariableClassTypeRepository; +import com.oguerreiro.resilient.service.dto.VariableClassTypeDTO; +import com.oguerreiro.resilient.service.mapper.VariableClassTypeMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.VariableClassType}. + */ +@Service +@Transactional +public class VariableClassTypeService extends AbstractResilientService { + + public VariableClassTypeService(VariableClassTypeRepository variableClassTypeRepository, + VariableClassTypeMapper variableClassTypeMapper) { + super(VariableClassType.class, variableClassTypeRepository, variableClassTypeMapper); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/VariableScopeService.java b/src/main/java/com/oguerreiro/resilient/service/VariableScopeService.java new file mode 100644 index 0000000..636420b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/VariableScopeService.java @@ -0,0 +1,54 @@ +package com.oguerreiro.resilient.service; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.repository.VariableScopeRepository; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; +import com.oguerreiro.resilient.service.mapper.VariableScopeMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.VariableScope}. + */ +@Service +@Transactional +public class VariableScopeService extends AbstractResilientService { + + private final VariableScopeRepository variableScopeRepository; + private final VariableScopeMapper variableScopeMapper; + + public VariableScopeService(VariableScopeRepository variableScopeRepository, + VariableScopeMapper variableScopeMapper) { + super(VariableScope.class, variableScopeRepository, variableScopeMapper); + + this.variableScopeRepository = variableScopeRepository; + this.variableScopeMapper = variableScopeMapper; + } + + @Transactional(readOnly = true) + public List findAllForData() { + log.debug("Request to get all findAllForData"); + + //@formatter:off + return variableScopeRepository.findByHiddenForDataIsFalseOrHiddenForDataIsNullOrderByCodeAsc() + .stream() + .map(variableScopeMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } + + @Transactional(readOnly = true) + public List findAllForFactor() { + log.debug("Request to get all findAllForFactor"); + + //@formatter:off + return variableScopeRepository.findByHiddenForFactorIsFalseOrHiddenForFactorIsNullOrderByCodeAsc() + .stream() + .map(variableScopeMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/VariableService.java b/src/main/java/com/oguerreiro/resilient/service/VariableService.java new file mode 100644 index 0000000..6383a46 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/VariableService.java @@ -0,0 +1,127 @@ +package com.oguerreiro.resilient.service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.repository.VariableRepository; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; +import com.oguerreiro.resilient.service.mapper.UnitMapper; +import com.oguerreiro.resilient.service.mapper.VariableMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.Variable}. + */ +@Service +@Transactional +public class VariableService extends AbstractResilientService { + private final VariableRepository variableRepository; + private final UnitMapper unitMapper; + private final VariableMapper variableMapper; + + public VariableService(VariableRepository variableRepository, VariableMapper variableMapper, UnitMapper unitMapper) { + super(Variable.class, variableRepository, variableMapper); + this.variableRepository = variableRepository; + this.unitMapper = unitMapper; + this.variableMapper = variableMapper; + } + + public Optional getVariableBaseUnit(Long variableId) { + Variable variable = variableRepository.findById(variableId).orElseThrow(); + UnitDTO unitDTO = this.unitMapper.toDto(variable.getBaseUnit()); + return Optional.of(unitDTO); + } + + public List findVariableForManualInput() { + log.debug("SERVICE request to findVariableForManualInput"); + //@formatter:off + return variableRepository.findAllForManualInput() + .stream() + .map(variableMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + //@formatter:on + } + + @Transactional(readOnly = true) + public byte[] exportVariables() { + //Build a Workbook + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("Input"); + exportVariable(sheet, true, false); + + sheet = workbook.createSheet("Output"); + exportVariable(sheet, false, true); + + workbook.write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to generate Excel file", e); + } + } + + private void exportVariable(Sheet sheet, Boolean input, Boolean output) { + // Header row + int c = 0; + Row headerRow = sheet.createRow(0); + headerRow.createCell(c++).setCellValue("Código"); + headerRow.createCell(c++).setCellValue("Nome"); + headerRow.createCell(c++).setCellValue("Descrição"); + + headerRow.createCell(c++).setCellValue("Unidade Base-Código"); + headerRow.createCell(c++).setCellValue("Unidade Base-Nome"); + headerRow.createCell(c++).setCellValue("Unidade Base-Simbolo"); + + headerRow.createCell(c++).setCellValue("Outras Unidades"); + headerRow.createCell(c++).setCellValue("Escala"); + + headerRow.createCell(c++).setCellValue("Âmbito-Código"); + headerRow.createCell(c++).setCellValue("Âmbito-Nome"); + + headerRow.createCell(c++).setCellValue("Categoria-Código"); + headerRow.createCell(c++).setCellValue("Categoria-Código"); + + headerRow.createCell(c++).setCellValue("Formula"); + + // Get data with a stream (query cursor) + try (Stream stream = this.variableRepository.streamAll(input, output)) { + // Loop data + stream.forEach(variable -> { + int cc = 0; + // Write row + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + row.createCell(cc++).setCellValue(variable.getCode()); + row.createCell(cc++).setCellValue(variable.getName()); + row.createCell(cc++).setCellValue(variable.getDescription()); + + row.createCell(cc++).setCellValue(variable.getBaseUnit().getCode()); + row.createCell(cc++).setCellValue(variable.getBaseUnit().getName()); + row.createCell(cc++).setCellValue(variable.getBaseUnit().getSymbol()); + + String codes = variable.getVariableUnits().stream().map(vu -> "\"" + vu.getUnit().getCode() + "\"").collect( + Collectors.joining("; ")); + row.createCell(cc++).setCellValue(codes); + row.createCell(cc++).setCellValue(variable.getValueScale()); + + row.createCell(cc++).setCellValue(variable.getVariableScope().getCode()); + row.createCell(cc++).setCellValue(variable.getVariableScope().getName()); + + row.createCell(cc++).setCellValue(variable.getVariableCategory().getCode()); + row.createCell(cc++).setCellValue(variable.getVariableCategory().getName()); + + row.createCell(cc++).setCellValue(variable.getOutputFormula()); + }); + } + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/VariableUnitsService.java b/src/main/java/com/oguerreiro/resilient/service/VariableUnitsService.java new file mode 100644 index 0000000..99c5846 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/VariableUnitsService.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.VariableUnits; +import com.oguerreiro.resilient.repository.VariableUnitsRepository; +import com.oguerreiro.resilient.service.dto.VariableUnitsDTO; +import com.oguerreiro.resilient.service.mapper.VariableUnitsMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.VariableUnits}. + */ +@Service +@Transactional +public class VariableUnitsService extends AbstractResilientService { + + public VariableUnitsService(VariableUnitsRepository variableUnitsRepository, + VariableUnitsMapper variableUnitsMapper) { + super(VariableUnits.class, variableUnitsRepository, variableUnitsMapper); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/AbstractCriteria.java b/src/main/java/com/oguerreiro/resilient/service/criteria/AbstractCriteria.java new file mode 100644 index 0000000..1c1d0ef --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/AbstractCriteria.java @@ -0,0 +1,78 @@ +package com.oguerreiro.resilient.service.criteria; + +import java.io.Serializable; +import java.util.Objects; +import java.util.Optional; + +import com.oguerreiro.resilient.domain.enumeration.DataSourceType; + +import tech.jhipster.service.Criteria; +import tech.jhipster.service.filter.Filter; + +public abstract class AbstractCriteria implements Serializable, Criteria { + private static final long serialVersionUID = 1L; + + /** + * Class for filtering DataSourceType + */ + public static class DataSourceTypeFilter extends Filter { + + private static final long serialVersionUID = 1L; + + public DataSourceTypeFilter() { + } + + public DataSourceTypeFilter(DataSourceTypeFilter filter) { + super(filter); + } + + @Override + public DataSourceTypeFilter copy() { + return new DataSourceTypeFilter(this); + } + } + + private Boolean distinct; + + public AbstractCriteria() { + } + + public AbstractCriteria(AbstractCriteria other) { + this.distinct = other.distinct; + } + + public Boolean getDistinct() { + return distinct; + } + + public Optional optionalDistinct() { + return Optional.ofNullable(distinct); + } + + public Boolean distinct() { + if (distinct == null) { + setDistinct(true); + } + return distinct; + } + + public void setDistinct(Boolean distinct) { + this.distinct = distinct; + } + + @Override + public boolean equals(Object o) { + final AbstractCriteria that = (AbstractCriteria) o; + return Objects.equals(distinct, that.distinct); + } + + @Override + public int hashCode() { + return Objects.hash(distinct); + } + + @Override + public String toString() { + return optionalDistinct().map(f -> "distinct=" + f + ", ").orElse(""); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/EmissionFactorCriteria.java b/src/main/java/com/oguerreiro/resilient/service/criteria/EmissionFactorCriteria.java new file mode 100644 index 0000000..a1e79c2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/EmissionFactorCriteria.java @@ -0,0 +1,324 @@ +package com.oguerreiro.resilient.service.criteria; + +import java.util.Objects; +import java.util.Optional; + +import org.springdoc.core.annotations.ParameterObject; + +import tech.jhipster.service.filter.BigDecimalFilter; +import tech.jhipster.service.filter.Filter; +import tech.jhipster.service.filter.IntegerFilter; +import tech.jhipster.service.filter.LongFilter; +import tech.jhipster.service.filter.StringFilter; + +/** + * Criteria class for the {@link com.oguerreiro.resilient.domain.EmissionFactor} + * entity. This class is used in + * {@link com.oguerreiro.resilient.web.rest.EmissionFactorResource} to receive all + * the possible filtering options from the Http GET request parameters. For + * example the following could be a valid request: + * {@code /emission-factor?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + * As Spring is unable to properly convert the types, unless specific + * {@link Filter} class are used, we need to use fix type specific filters. + */ +@ParameterObject +public class EmissionFactorCriteria extends AbstractCriteria { + private static final long serialVersionUID = 1L; + + private LongFilter id; + + private IntegerFilter year; + + private StringFilter emissionFactorNameOrCode; + + private StringFilter code; + + private StringFilter name; + + private StringFilter unit; + + private StringFilter source; + + private StringFilter comments; + + private LongFilter variableScopeId; + + private LongFilter variableCategoryId; + + private BigDecimalFilter value; + + public EmissionFactorCriteria() { + } + + public EmissionFactorCriteria(EmissionFactorCriteria other) { + super(other); + + this.id = other.optionalId().map(LongFilter::copy).orElse(null); + this.year = other.optionalYear().map(IntegerFilter::copy).orElse(null); + this.emissionFactorNameOrCode = other.optionalEmissionFactorNameOrCode().map(StringFilter::copy).orElse(null); + this.code = other.optionalCode().map(StringFilter::copy).orElse(null); + this.name = other.optionalName().map(StringFilter::copy).orElse(null); + this.unit = other.optionalUnit().map(StringFilter::copy).orElse(null); + this.source = other.optionalSource().map(StringFilter::copy).orElse(null); + this.comments = other.optionalComments().map(StringFilter::copy).orElse(null); + this.variableScopeId = other.optionalVariableScopeId().map(LongFilter::copy).orElse(null); + this.variableCategoryId = other.optionalVariableCategoryId().map(LongFilter::copy).orElse(null); + this.value = other.optionalValue().map(BigDecimalFilter::copy).orElse(null); + } + + @Override + public EmissionFactorCriteria copy() { + return new EmissionFactorCriteria(this); + } + + public LongFilter getId() { + return id; + } + + public Optional optionalId() { + return Optional.ofNullable(id); + } + + public LongFilter id() { + if (id == null) { + setId(new LongFilter()); + } + return id; + } + + public void setId(LongFilter id) { + this.id = id; + } + + public IntegerFilter getYear() { + return year; + } + + public Optional optionalYear() { + return Optional.ofNullable(year); + } + + public IntegerFilter year() { + if (year == null) { + setYear(new IntegerFilter()); + } + return year; + } + + public void setYear(IntegerFilter year) { + this.year = year; + } + + public StringFilter getEmissionFactorNameOrCode() { + return emissionFactorNameOrCode; + } + + public Optional optionalEmissionFactorNameOrCode() { + return Optional.ofNullable(emissionFactorNameOrCode); + } + + public StringFilter emissionFactorNameOrCode() { + if (emissionFactorNameOrCode == null) { + setEmissionFactorNameOrCode(new StringFilter()); + } + return emissionFactorNameOrCode; + } + + public void setEmissionFactorNameOrCode(StringFilter emissionFactorNameOrCode) { + this.emissionFactorNameOrCode = emissionFactorNameOrCode; + } + + public StringFilter getCode() { + return code; + } + + public Optional optionalCode() { + return Optional.ofNullable(code); + } + + public StringFilter code() { + if (code == null) { + setCode(new StringFilter()); + } + return code; + } + + public void setCode(StringFilter code) { + this.code = code; + } + + public StringFilter getName() { + return name; + } + + public Optional optionalName() { + return Optional.ofNullable(name); + } + + public StringFilter name() { + if (name == null) { + setName(new StringFilter()); + } + return name; + } + + public void setName(StringFilter name) { + this.name = name; + } + + public StringFilter getUnit() { + return unit; + } + + public Optional optionalUnit() { + return Optional.ofNullable(unit); + } + + public StringFilter unit() { + if (unit == null) { + setUnit(new StringFilter()); + } + return unit; + } + + public void setUnit(StringFilter unit) { + this.unit = unit; + } + + public StringFilter getSource() { + return source; + } + + public Optional optionalSource() { + return Optional.ofNullable(source); + } + + public StringFilter souce() { + if (source == null) { + setSource(new StringFilter()); + } + return source; + } + + public void setSource(StringFilter source) { + this.source = source; + } + + public StringFilter getComments() { + return comments; + } + + public Optional optionalComments() { + return Optional.ofNullable(comments); + } + + public StringFilter comments() { + if (comments == null) { + setComments(new StringFilter()); + } + return comments; + } + + public void setComments(StringFilter comments) { + this.comments = comments; + } + + public LongFilter getVariableScopeId() { + return variableScopeId; + } + + public Optional optionalVariableScopeId() { + return Optional.ofNullable(variableScopeId); + } + + public LongFilter variableScopeId() { + if (variableScopeId == null) { + setVariableScopeId(new LongFilter()); + } + return variableScopeId; + } + + public void setVariableScopeId(LongFilter variableScopeId) { + this.variableScopeId = variableScopeId; + } + + public LongFilter getVariableCategoryId() { + return variableCategoryId; + } + + public Optional optionalVariableCategoryId() { + return Optional.ofNullable(variableCategoryId); + } + + public LongFilter variableCategoryId() { + if (variableCategoryId == null) { + setVariableCategoryId(new LongFilter()); + } + return variableCategoryId; + } + + public void setVariableCategoryId(LongFilter variableCategoryId) { + this.variableCategoryId = variableCategoryId; + } + + public BigDecimalFilter getValue() { + return value; + } + + public Optional optionalValue() { + return Optional.ofNullable(value); + } + + public BigDecimalFilter value() { + if (value == null) { + setValue(new BigDecimalFilter()); + } + return value; + } + + public void setValue(BigDecimalFilter value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final EmissionFactorCriteria that = (EmissionFactorCriteria) o; + return (Objects.equals(id, that.id) && Objects.equals(year, that.year) + && Objects.equals(emissionFactorNameOrCode, that.emissionFactorNameOrCode) && Objects.equals(code, that.code) + && Objects.equals(name, that.name) && Objects.equals(unit, that.unit) && Objects.equals(source, that.source) + && Objects.equals(comments, that.comments) && Objects.equals(variableScopeId, that.variableScopeId) + && Objects.equals(variableCategoryId, that.variableCategoryId) && Objects.equals(value, that.value) + && super.equals(o)); + } + + @Override + public int hashCode() { + return Objects.hash(id, year, emissionFactorNameOrCode, code, name, unit, source, comments, variableScopeId, + variableCategoryId, value, super.hashCode()); + } + + // prettier-ignore + @Override + public String toString() { + //@formatter:off + return "InputDataCriteria{" + optionalId().map(f -> "id=" + f + ", ").orElse("") + + optionalYear().map(f -> "year=" + f + ", ").orElse("") + + optionalEmissionFactorNameOrCode().map(f -> "emissionFactorNameOrCode=" + f + ", ").orElse("") + + optionalCode().map(f -> "code=" + f + ", ").orElse("") + + optionalName().map(f -> "name=" + f + ", ").orElse("") + + optionalUnit().map(f -> "unit=" + f + ", ").orElse("") + + optionalSource().map(f -> "source=" + f + ", ").orElse("") + + optionalComments().map(f -> "comments=" + f + ", ").orElse("") + + optionalVariableScopeId().map(f -> "variableScopeId=" + f + ", ").orElse("") + + optionalVariableCategoryId().map(f -> "variableCategoryId=" + f + ", ").orElse("") + + optionalValue().map(f -> "value=" + f + ", ").orElse("") + + super.toString() + "}"; + //@formatter:on + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/InputDataCriteria.java b/src/main/java/com/oguerreiro/resilient/service/criteria/InputDataCriteria.java new file mode 100644 index 0000000..8b27fb1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/InputDataCriteria.java @@ -0,0 +1,729 @@ +package com.oguerreiro.resilient.service.criteria; + +import java.util.Objects; +import java.util.Optional; + +import org.springdoc.core.annotations.ParameterObject; + +import tech.jhipster.service.filter.BigDecimalFilter; +import tech.jhipster.service.filter.Filter; +import tech.jhipster.service.filter.InstantFilter; +import tech.jhipster.service.filter.IntegerFilter; +import tech.jhipster.service.filter.LocalDateFilter; +import tech.jhipster.service.filter.LongFilter; +import tech.jhipster.service.filter.StringFilter; + +/** + * Criteria class for the {@link com.oguerreiro.resilient.domain.InputData} + * entity. This class is used in + * {@link com.oguerreiro.resilient.web.rest.InputDataResource} to receive all + * the possible filtering options from the Http GET request parameters. For + * example the following could be a valid request: + * {@code /input-data?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + * As Spring is unable to properly convert the types, unless specific + * {@link Filter} class are used, we need to use fix type specific filters. + */ +@ParameterObject +public class InputDataCriteria extends AbstractCriteria { + private static final long serialVersionUID = 1L; + + private LongFilter id; + + private BigDecimalFilter sourceValue; + + private BigDecimalFilter variableValue; + + private BigDecimalFilter imputedValue; + + private DataSourceTypeFilter sourceType; + + private LocalDateFilter dataDate; + + private InstantFilter changeDate; + + private StringFilter changeUsername; + + private StringFilter dataSource; + + private StringFilter dataUser; + + private StringFilter dataComments; + + private InstantFilter creationDate; + + private StringFilter creationUsername; + + private IntegerFilter version; + + private LongFilter inputDataId; + + private LongFilter variableId; + + private StringFilter variableNameOrCode; + + private StringFilter variableClassNameOrCode; + + private LongFilter periodId; + + private LongFilter sourceUnitId; + + private LongFilter unitId; + + private LongFilter ownerId; + + private LongFilter sourceInputDataId; + + private LongFilter sourceInputDataUploadId; + + //TODO: Change this to variableScopeId. No sense in changing the real domain name (VariableScope) + @Deprecated + private LongFilter scopeId; + //TODO: Change this to variableCategoryId. No sense in changing the real domain name (VariableCategory) + @Deprecated + private LongFilter categoryId; + private LongFilter variableScopeId; + private LongFilter variableCategoryId; + + public InputDataCriteria() { + } + + public InputDataCriteria(InputDataCriteria other) { + super(other); + + this.id = other.optionalId().map(LongFilter::copy).orElse(null); + this.sourceValue = other.optionalSourceValue().map(BigDecimalFilter::copy).orElse(null); + this.variableValue = other.optionalVariableValue().map(BigDecimalFilter::copy).orElse(null); + this.imputedValue = other.optionalImputedValue().map(BigDecimalFilter::copy).orElse(null); + this.sourceType = other.optionalSourceType().map(DataSourceTypeFilter::copy).orElse(null); + this.dataDate = other.optionalDataDate().map(LocalDateFilter::copy).orElse(null); + this.changeDate = other.optionalChangeDate().map(InstantFilter::copy).orElse(null); + this.changeUsername = other.optionalChangeUsername().map(StringFilter::copy).orElse(null); + this.dataSource = other.optionalDataSource().map(StringFilter::copy).orElse(null); + this.dataUser = other.optionalDataUser().map(StringFilter::copy).orElse(null); + this.dataComments = other.optionalDataComments().map(StringFilter::copy).orElse(null); + this.creationDate = other.optionalCreationDate().map(InstantFilter::copy).orElse(null); + this.creationUsername = other.optionalCreationUsername().map(StringFilter::copy).orElse(null); + this.version = other.optionalVersion().map(IntegerFilter::copy).orElse(null); + this.inputDataId = other.optionalInputDataId().map(LongFilter::copy).orElse(null); + this.variableId = other.optionalVariableId().map(LongFilter::copy).orElse(null); + this.variableNameOrCode = other.optionalVariableNameOrCode().map(StringFilter::copy).orElse(null); + this.variableClassNameOrCode = other.optionalVariableClassNameOrCode().map(StringFilter::copy).orElse(null); + this.periodId = other.optionalPeriodId().map(LongFilter::copy).orElse(null); + this.sourceUnitId = other.optionalSourceUnitId().map(LongFilter::copy).orElse(null); + this.unitId = other.optionalUnitId().map(LongFilter::copy).orElse(null); + this.ownerId = other.optionalOwnerId().map(LongFilter::copy).orElse(null); + this.sourceInputDataId = other.optionalSourceInputDataId().map(LongFilter::copy).orElse(null); + this.sourceInputDataUploadId = other.optionalSourceInputDataUploadId().map(LongFilter::copy).orElse(null); + + this.scopeId = other.optionalScopeId().map(LongFilter::copy).orElse(null); + this.categoryId = other.optionalCategoryId().map(LongFilter::copy).orElse(null); + this.variableScopeId = other.optionalVariableScopeId().map(LongFilter::copy).orElse(null); + this.variableCategoryId = other.optionalVariableCategoryId().map(LongFilter::copy).orElse(null); + } + + @Override + public InputDataCriteria copy() { + return new InputDataCriteria(this); + } + + public LongFilter getId() { + return id; + } + + public Optional optionalId() { + return Optional.ofNullable(id); + } + + public LongFilter id() { + if (id == null) { + setId(new LongFilter()); + } + return id; + } + + public void setId(LongFilter id) { + this.id = id; + } + + public BigDecimalFilter getSourceValue() { + return sourceValue; + } + + public Optional optionalSourceValue() { + return Optional.ofNullable(sourceValue); + } + + public BigDecimalFilter sourceValue() { + if (sourceValue == null) { + setSourceValue(new BigDecimalFilter()); + } + return sourceValue; + } + + public void setSourceValue(BigDecimalFilter sourceValue) { + this.sourceValue = sourceValue; + } + + public BigDecimalFilter getVariableValue() { + return variableValue; + } + + public Optional optionalVariableValue() { + return Optional.ofNullable(variableValue); + } + + public BigDecimalFilter variableValue() { + if (variableValue == null) { + setVariableValue(new BigDecimalFilter()); + } + return variableValue; + } + + public void setVariableValue(BigDecimalFilter variableValue) { + this.variableValue = variableValue; + } + + public BigDecimalFilter getImputedValue() { + return imputedValue; + } + + public Optional optionalImputedValue() { + return Optional.ofNullable(imputedValue); + } + + public BigDecimalFilter imputedValue() { + if (imputedValue == null) { + setImputedValue(new BigDecimalFilter()); + } + return imputedValue; + } + + public void setImputedValue(BigDecimalFilter imputedValue) { + this.imputedValue = imputedValue; + } + + public DataSourceTypeFilter getSourceType() { + return sourceType; + } + + public Optional optionalSourceType() { + return Optional.ofNullable(sourceType); + } + + public DataSourceTypeFilter sourceType() { + if (sourceType == null) { + setSourceType(new DataSourceTypeFilter()); + } + return sourceType; + } + + public void setSourceType(DataSourceTypeFilter sourceType) { + this.sourceType = sourceType; + } + + public LocalDateFilter getDataDate() { + return dataDate; + } + + public Optional optionalDataDate() { + return Optional.ofNullable(dataDate); + } + + public LocalDateFilter dataDate() { + if (dataDate == null) { + setDataDate(new LocalDateFilter()); + } + return dataDate; + } + + public void setDataDate(LocalDateFilter dataDate) { + this.dataDate = dataDate; + } + + public InstantFilter getChangeDate() { + return changeDate; + } + + public Optional optionalChangeDate() { + return Optional.ofNullable(changeDate); + } + + public InstantFilter changeDate() { + if (changeDate == null) { + setChangeDate(new InstantFilter()); + } + return changeDate; + } + + public void setChangeDate(InstantFilter changeDate) { + this.changeDate = changeDate; + } + + public StringFilter getChangeUsername() { + return changeUsername; + } + + public Optional optionalChangeUsername() { + return Optional.ofNullable(changeUsername); + } + + public StringFilter changeUsername() { + if (changeUsername == null) { + setChangeUsername(new StringFilter()); + } + return changeUsername; + } + + public void setChangeUsername(StringFilter changeUsername) { + this.changeUsername = changeUsername; + } + + public StringFilter getDataSource() { + return dataSource; + } + + public Optional optionalDataSource() { + return Optional.ofNullable(dataSource); + } + + public StringFilter dataSource() { + if (dataSource == null) { + setDataSource(new StringFilter()); + } + return dataSource; + } + + public void setDataSource(StringFilter dataSource) { + this.dataSource = dataSource; + } + + public StringFilter getDataUser() { + return dataUser; + } + + public Optional optionalDataUser() { + return Optional.ofNullable(dataUser); + } + + public StringFilter dataUser() { + if (dataUser == null) { + setDataUser(new StringFilter()); + } + return dataUser; + } + + public void setDataUser(StringFilter dataUser) { + this.dataUser = dataUser; + } + + public StringFilter getDataComments() { + return dataComments; + } + + public Optional optionalDataComments() { + return Optional.ofNullable(dataComments); + } + + public StringFilter dataComments() { + if (dataComments == null) { + setDataComments(new StringFilter()); + } + return dataComments; + } + + public void setDataComments(StringFilter dataComments) { + this.dataComments = dataComments; + } + + public InstantFilter getCreationDate() { + return creationDate; + } + + public Optional optionalCreationDate() { + return Optional.ofNullable(creationDate); + } + + public InstantFilter creationDate() { + if (creationDate == null) { + setCreationDate(new InstantFilter()); + } + return creationDate; + } + + public void setCreationDate(InstantFilter creationDate) { + this.creationDate = creationDate; + } + + public StringFilter getCreationUsername() { + return creationUsername; + } + + public Optional optionalCreationUsername() { + return Optional.ofNullable(creationUsername); + } + + public StringFilter creationUsername() { + if (creationUsername == null) { + setCreationUsername(new StringFilter()); + } + return creationUsername; + } + + public void setCreationUsername(StringFilter creationUsername) { + this.creationUsername = creationUsername; + } + + public IntegerFilter getVersion() { + return version; + } + + public Optional optionalVersion() { + return Optional.ofNullable(version); + } + + public IntegerFilter version() { + if (version == null) { + setVersion(new IntegerFilter()); + } + return version; + } + + public void setVersion(IntegerFilter version) { + this.version = version; + } + + public LongFilter getInputDataId() { + return inputDataId; + } + + public Optional optionalInputDataId() { + return Optional.ofNullable(inputDataId); + } + + public LongFilter inputDataId() { + if (inputDataId == null) { + setInputDataId(new LongFilter()); + } + return inputDataId; + } + + public void setInputDataId(LongFilter inputDataId) { + this.inputDataId = inputDataId; + } + + public LongFilter getVariableId() { + return variableId; + } + + public Optional optionalVariableId() { + return Optional.ofNullable(variableId); + } + + public LongFilter variableId() { + if (variableId == null) { + setVariableId(new LongFilter()); + } + return variableId; + } + + public void setVariableId(LongFilter variableId) { + this.variableId = variableId; + } + + public StringFilter getVariableNameOrCode() { + return variableNameOrCode; + } + + public Optional optionalVariableNameOrCode() { + return Optional.ofNullable(variableNameOrCode); + } + + public StringFilter variableNameOrCode() { + if (variableNameOrCode == null) { + setVariableNameOrCode(new StringFilter()); + } + return variableNameOrCode; + } + + public void setVariableNameOrCode(StringFilter variableNameOrCode) { + this.variableNameOrCode = variableNameOrCode; + } + + public StringFilter getVariableClassNameOrCode() { + return variableClassNameOrCode; + } + + public Optional optionalVariableClassNameOrCode() { + return Optional.ofNullable(variableClassNameOrCode); + } + + public StringFilter variableClassNameOrCode() { + if (variableClassNameOrCode == null) { + setVariableClassNameOrCode(new StringFilter()); + } + return variableClassNameOrCode; + } + + public void setVariableClassNameOrCode(StringFilter variableClassNameOrCode) { + this.variableClassNameOrCode = variableClassNameOrCode; + } + + public LongFilter getPeriodId() { + return periodId; + } + + public Optional optionalPeriodId() { + return Optional.ofNullable(periodId); + } + + public LongFilter periodId() { + if (periodId == null) { + setPeriodId(new LongFilter()); + } + return periodId; + } + + public void setPeriodId(LongFilter periodId) { + this.periodId = periodId; + } + + public LongFilter getSourceUnitId() { + return sourceUnitId; + } + + public Optional optionalSourceUnitId() { + return Optional.ofNullable(sourceUnitId); + } + + public LongFilter sourceUnitId() { + if (sourceUnitId == null) { + setSourceUnitId(new LongFilter()); + } + return sourceUnitId; + } + + public void setSourceUnitId(LongFilter sourceUnitId) { + this.sourceUnitId = sourceUnitId; + } + + public LongFilter getUnitId() { + return unitId; + } + + public Optional optionalUnitId() { + return Optional.ofNullable(unitId); + } + + public LongFilter unitId() { + if (unitId == null) { + setUnitId(new LongFilter()); + } + return unitId; + } + + public void setUnitId(LongFilter unitId) { + this.unitId = unitId; + } + + public LongFilter getOwnerId() { + return ownerId; + } + + public Optional optionalOwnerId() { + return Optional.ofNullable(ownerId); + } + + public LongFilter ownerId() { + if (ownerId == null) { + setOwnerId(new LongFilter()); + } + return ownerId; + } + + public void setOwnerId(LongFilter ownerId) { + this.ownerId = ownerId; + } + + public LongFilter getScopeId() { + return scopeId; + } + + public Optional optionalScopeId() { + return Optional.ofNullable(scopeId); + } + + public LongFilter scopeId() { + if (scopeId == null) { + setScopeId(new LongFilter()); + } + return scopeId; + } + + public void setScopeId(LongFilter scopeId) { + this.scopeId = scopeId; + } + + public LongFilter getCategoryId() { + return categoryId; + } + + public Optional optionalCategoryId() { + return Optional.ofNullable(categoryId); + } + + public LongFilter categoryId() { + if (categoryId == null) { + setCategoryId(new LongFilter()); + } + return categoryId; + } + + public void setCategoryId(LongFilter categoryId) { + this.categoryId = categoryId; + } + + public LongFilter getVariableScopeId() { + return variableScopeId; + } + + public Optional optionalVariableScopeId() { + return Optional.ofNullable(variableScopeId); + } + + public LongFilter variableScopeId() { + if (scopeId == null) { + setVariableScopeId(new LongFilter()); + } + return variableScopeId; + } + + public void setVariableScopeId(LongFilter variableScopeId) { + this.variableScopeId = variableScopeId; + } + + public LongFilter getVariableCategoryId() { + return variableCategoryId; + } + + public Optional optionalVariableCategoryId() { + return Optional.ofNullable(variableCategoryId); + } + + public LongFilter variableCategoryId() { + if (variableCategoryId == null) { + setVariableCategoryId(new LongFilter()); + } + return variableCategoryId; + } + + public void setVariableCategoryId(LongFilter variableCategoryId) { + this.variableCategoryId = variableCategoryId; + } + + public LongFilter getSourceInputDataId() { + return sourceInputDataId; + } + + public Optional optionalSourceInputDataId() { + return Optional.ofNullable(sourceInputDataId); + } + + public LongFilter sourceInputDataId() { + if (sourceInputDataId == null) { + setSourceInputDataId(new LongFilter()); + } + return sourceInputDataId; + } + + public void setSourceInputDataId(LongFilter sourceInputDataId) { + this.sourceInputDataId = sourceInputDataId; + } + + public LongFilter getSourceInputDataUploadId() { + return sourceInputDataUploadId; + } + + public Optional optionalSourceInputDataUploadId() { + return Optional.ofNullable(sourceInputDataUploadId); + } + + public LongFilter sourceInputDataUploadId() { + if (sourceInputDataUploadId == null) { + setSourceInputDataUploadId(new LongFilter()); + } + return sourceInputDataUploadId; + } + + public void setSourceInputDataUploadId(LongFilter sourceInputDataUploadId) { + this.sourceInputDataUploadId = sourceInputDataUploadId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final InputDataCriteria that = (InputDataCriteria) o; + // @formatter:off + return (Objects.equals(id, that.id) && Objects.equals(sourceValue, that.sourceValue) + && Objects.equals(variableValue, that.variableValue) && Objects.equals(imputedValue, that.imputedValue) + && Objects.equals(sourceType, that.sourceType) && Objects.equals(dataDate, that.dataDate) + && Objects.equals(changeDate, that.changeDate) && Objects.equals(changeUsername, that.changeUsername) + && Objects.equals(dataSource, that.dataSource) && Objects.equals(dataUser, that.dataUser) + && Objects.equals(dataComments, that.dataComments) && Objects.equals(creationDate, that.creationDate) + && Objects.equals(creationUsername, that.creationUsername) && Objects.equals(version, that.version) + && Objects.equals(inputDataId, that.inputDataId) && Objects.equals(variableId, that.variableId) + && Objects.equals(variableNameOrCode, that.variableNameOrCode) && Objects.equals(periodId, that.periodId) + && Objects.equals(variableClassNameOrCode, that.variableClassNameOrCode) && Objects.equals(unitId, that.unitId) + && Objects.equals(ownerId, that.ownerId) && Objects.equals(sourceInputDataId, that.sourceInputDataId) + && Objects.equals(sourceInputDataUploadId, that.sourceInputDataUploadId) + && Objects.equals(scopeId, that.scopeId) + && Objects.equals(categoryId, that.categoryId) + && Objects.equals(variableScopeId, that.variableScopeId) + && Objects.equals(variableCategoryId, that.variableCategoryId) && super.equals(o)); + // @formatter:on + } + + @Override + public int hashCode() { + return Objects.hash(id, sourceValue, variableValue, imputedValue, sourceType, dataDate, changeDate, changeUsername, + dataSource, dataUser, dataComments, creationDate, creationUsername, version, inputDataId, variableId, + variableNameOrCode, variableClassNameOrCode, periodId, sourceUnitId, unitId, ownerId, sourceInputDataId, + sourceInputDataUploadId, super.hashCode()); + } + + // prettier-ignore + @Override + public String toString() { + return "InputDataCriteria{" + optionalId().map(f -> "id=" + f + ", ").orElse("") + + optionalSourceValue().map(f -> "sourceValue=" + f + ", ").orElse("") + + optionalVariableValue().map(f -> "variableValue=" + f + ", ").orElse("") + + optionalImputedValue().map(f -> "imputedValue=" + f + ", ").orElse("") + + optionalSourceType().map(f -> "sourceType=" + f + ", ").orElse("") + + optionalDataDate().map(f -> "dataDate=" + f + ", ").orElse("") + + optionalChangeDate().map(f -> "changeDate=" + f + ", ").orElse("") + + optionalChangeUsername().map(f -> "changeUsername=" + f + ", ").orElse("") + + optionalDataSource().map(f -> "dataSource=" + f + ", ").orElse("") + + optionalDataUser().map(f -> "dataUser=" + f + ", ").orElse("") + + optionalDataComments().map(f -> "dataComments=" + f + ", ").orElse("") + + optionalCreationDate().map(f -> "creationDate=" + f + ", ").orElse("") + + optionalCreationUsername().map(f -> "creationUsername=" + f + ", ").orElse("") + + optionalVersion().map(f -> "version=" + f + ", ").orElse("") + + optionalInputDataId().map(f -> "inputDataId=" + f + ", ").orElse("") + + optionalVariableId().map(f -> "variableId=" + f + ", ").orElse("") + + optionalVariableNameOrCode().map(f -> "variableNameOrCode=" + f + ", ").orElse("") + + optionalVariableClassNameOrCode().map(f -> "variableClassNameOrCode=" + f + ", ").orElse("") + + optionalPeriodId().map(f -> "periodId=" + f + ", ").orElse("") + + optionalSourceUnitId().map(f -> "sourceUnitId=" + f + ", ").orElse("") + + optionalUnitId().map(f -> "unitId=" + f + ", ").orElse("") + + optionalOwnerId().map(f -> "ownerId=" + f + ", ").orElse("") + + optionalScopeId().map(f -> "scopeId=" + f + ", ").orElse("") + + optionalCategoryId().map(f -> "categoryId=" + f + ", ").orElse("") + + optionalSourceInputDataId().map(f -> "sourceInputDataId=" + f + ", ").orElse("") + + optionalSourceInputDataUploadId().map(f -> "sourceInputDataUploadId=" + f + ", ").orElse("") + + super.toString() + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/OutputDataCriteria.java b/src/main/java/com/oguerreiro/resilient/service/criteria/OutputDataCriteria.java new file mode 100644 index 0000000..1025ebc --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/OutputDataCriteria.java @@ -0,0 +1,255 @@ +package com.oguerreiro.resilient.service.criteria; + +import java.util.Objects; +import java.util.Optional; + +import org.springdoc.core.annotations.ParameterObject; + +import tech.jhipster.service.filter.Filter; +import tech.jhipster.service.filter.LongFilter; +import tech.jhipster.service.filter.StringFilter; + +/** + * Criteria class for the {@link com.oguerreiro.resilient.domain.OutputData} + * entity. This class is used in + * {@link com.oguerreiro.resilient.web.rest.OutputDataResource} to receive all + * the possible filtering options from the Http GET request parameters. For + * example the following could be a valid request: + * {@code /input-data?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + * As Spring is unable to properly convert the types, unless specific + * {@link Filter} class are used, we need to use fix type specific filters. + */ +@ParameterObject +public class OutputDataCriteria extends AbstractCriteria { + private static final long serialVersionUID = 1L; + + private LongFilter id; + + private LongFilter variableId; + + private StringFilter variableNameOrCode; + + private LongFilter periodId; + + private LongFilter periodVersionId; + + private LongFilter ownerId; + + private LongFilter variableScopeId; + private LongFilter variableCategoryId; + + public OutputDataCriteria() { + } + + public OutputDataCriteria(OutputDataCriteria other) { + super(other); + + this.id = other.optionalId().map(LongFilter::copy).orElse(null); + this.variableId = other.optionalVariableId().map(LongFilter::copy).orElse(null); + this.variableNameOrCode = other.optionalVariableNameOrCode().map(StringFilter::copy).orElse(null); + this.periodId = other.optionalPeriodId().map(LongFilter::copy).orElse(null); + this.periodVersionId = other.optionalPeriodVersionId().map(LongFilter::copy).orElse(null); + this.ownerId = other.optionalOwnerId().map(LongFilter::copy).orElse(null); + this.variableScopeId = other.optionalVariableScopeId().map(LongFilter::copy).orElse(null); + this.variableCategoryId = other.optionalVariableCategoryId().map(LongFilter::copy).orElse(null); + } + + @Override + public OutputDataCriteria copy() { + return new OutputDataCriteria(this); + } + + public LongFilter getId() { + return id; + } + + public Optional optionalId() { + return Optional.ofNullable(id); + } + + public LongFilter id() { + if (id == null) { + setId(new LongFilter()); + } + return id; + } + + public void setId(LongFilter id) { + this.id = id; + } + + public LongFilter getVariableId() { + return variableId; + } + + public Optional optionalVariableId() { + return Optional.ofNullable(variableId); + } + + public LongFilter variableId() { + if (variableId == null) { + setVariableId(new LongFilter()); + } + return variableId; + } + + public void setVariableId(LongFilter variableId) { + this.variableId = variableId; + } + + public StringFilter getVariableNameOrCode() { + return variableNameOrCode; + } + + public Optional optionalVariableNameOrCode() { + return Optional.ofNullable(variableNameOrCode); + } + + public StringFilter variableNameOrCode() { + if (variableNameOrCode == null) { + setVariableNameOrCode(new StringFilter()); + } + return variableNameOrCode; + } + + public void setVariableNameOrCode(StringFilter variableNameOrCode) { + this.variableNameOrCode = variableNameOrCode; + } + + public LongFilter getPeriodId() { + return periodId; + } + + public Optional optionalPeriodId() { + return Optional.ofNullable(periodId); + } + + public LongFilter periodId() { + if (periodId == null) { + setPeriodId(new LongFilter()); + } + return periodId; + } + + public void setPeriodId(LongFilter periodId) { + this.periodId = periodId; + } + + public LongFilter getPeriodVersionId() { + return periodVersionId; + } + + public Optional optionalPeriodVersionId() { + return Optional.ofNullable(periodVersionId); + } + + public LongFilter periodVersionId() { + if (periodVersionId == null) { + setPeriodVersionId(new LongFilter()); + } + return periodVersionId; + } + + public void setPeriodVersionId(LongFilter periodVersionId) { + this.periodVersionId = periodVersionId; + } + + public LongFilter getOwnerId() { + return ownerId; + } + + public Optional optionalOwnerId() { + return Optional.ofNullable(ownerId); + } + + public LongFilter ownerId() { + if (ownerId == null) { + setOwnerId(new LongFilter()); + } + return ownerId; + } + + public void setOwnerId(LongFilter ownerId) { + this.ownerId = ownerId; + } + + public LongFilter getVariableScopeId() { + return variableScopeId; + } + + public Optional optionalVariableScopeId() { + return Optional.ofNullable(variableScopeId); + } + + public LongFilter variableScopeId() { + if (variableScopeId == null) { + setVariableScopeId(new LongFilter()); + } + return variableScopeId; + } + + public void setVariableScopeId(LongFilter variableScopeId) { + this.variableScopeId = variableScopeId; + } + + public LongFilter getVariableCategoryId() { + return variableCategoryId; + } + + public Optional optionalVariableCategoryId() { + return Optional.ofNullable(variableCategoryId); + } + + public LongFilter variableCategoryId() { + if (variableCategoryId == null) { + setVariableCategoryId(new LongFilter()); + } + return variableCategoryId; + } + + public void setVariableCategoryId(LongFilter variableCategoryId) { + this.variableCategoryId = variableCategoryId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final OutputDataCriteria that = (OutputDataCriteria) o; + + // @formatter:off + return (Objects.equals(id, that.id) + && Objects.equals(variableId, that.variableId) + && Objects.equals(variableScopeId, that.variableScopeId) + && Objects.equals(variableCategoryId, that.variableCategoryId) + && Objects.equals(variableNameOrCode, that.variableNameOrCode) + && Objects.equals(periodId, that.periodId) + && Objects.equals(periodVersionId, that.periodVersionId) + && Objects.equals(ownerId, that.ownerId) + && super.equals(o)); + // @formatter:on + } + + @Override + public int hashCode() { + return Objects.hash(id, variableId, variableScopeId, variableCategoryId, variableNameOrCode, periodId, + periodVersionId, ownerId, super.hashCode()); + } + + // prettier-ignore + @Override + public String toString() { + return "InputDataCriteria{" + optionalId().map(f -> "id=" + f + ", ").orElse("") + + optionalVariableId().map(f -> "variableId=" + f + ", ").orElse("") + + optionalVariableScopeId().map(f -> "variableScopeId=" + f + ", ").orElse("") + + optionalVariableCategoryId().map(f -> "variableCategoryId=" + f + ", ").orElse("") + + optionalVariableNameOrCode().map(f -> "variableNameOrCode=" + f + ", ").orElse("") + + optionalPeriodId().map(f -> "periodId=" + f + ", ").orElse("") + + optionalPeriodVersionId().map(f -> "periodVersionId=" + f + ", ").orElse("") + + optionalOwnerId().map(f -> "ownerId=" + f + ", ").orElse("") + super.toString() + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/UnitConverterCriteria.java b/src/main/java/com/oguerreiro/resilient/service/criteria/UnitConverterCriteria.java new file mode 100644 index 0000000..0bc9960 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/UnitConverterCriteria.java @@ -0,0 +1,178 @@ +package com.oguerreiro.resilient.service.criteria; + +import java.util.Objects; +import java.util.Optional; + +import org.springdoc.core.annotations.ParameterObject; + +import tech.jhipster.service.filter.Filter; +import tech.jhipster.service.filter.LongFilter; +import tech.jhipster.service.filter.StringFilter; + +/** + * Criteria class for the {@link com.oguerreiro.resilient.domain.UnitConverter} + * entity. This class is used in + * {@link com.oguerreiro.resilient.web.rest.UnitConverterResource} to receive all + * the possible filtering options from the Http GET request parameters. For + * example the following could be a valid request: + * {@code /input-data?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + * As Spring is unable to properly convert the types, unless specific + * {@link Filter} class are used, we need to use fix type specific filters. + */ +@ParameterObject +public class UnitConverterCriteria extends AbstractCriteria { + private static final long serialVersionUID = 1L; + + private LongFilter id; + + private StringFilter name; + + private StringFilter description; + + private LongFilter fromUnitId; + + private LongFilter toUnitId; + + public UnitConverterCriteria() { + super(); + } + + public UnitConverterCriteria(UnitConverterCriteria other) { + super(other); + + this.id = other.optionalId().map(LongFilter::copy).orElse(null); + this.name = other.optionalName().map(StringFilter::copy).orElse(null); + this.description = other.optionalDescription().map(StringFilter::copy).orElse(null); + this.fromUnitId = other.optionalFromUnitId().map(LongFilter::copy).orElse(null); + this.toUnitId = other.optionalToUnitId().map(LongFilter::copy).orElse(null); + } + + @Override + public UnitConverterCriteria copy() { + return new UnitConverterCriteria(this); + } + + public LongFilter getId() { + return id; + } + + public Optional optionalId() { + return Optional.ofNullable(id); + } + + public LongFilter id() { + if (id == null) { + setId(new LongFilter()); + } + return id; + } + + public void setId(LongFilter id) { + this.id = id; + } + + public StringFilter getName() { + return name; + } + + public Optional optionalName() { + return Optional.ofNullable(name); + } + + public StringFilter name() { + if (name == null) { + setName(new StringFilter()); + } + return name; + } + + public void setName(StringFilter name) { + this.name = name; + } + + public StringFilter getDescription() { + return description; + } + + public Optional optionalDescription() { + return Optional.ofNullable(description); + } + + public StringFilter description() { + if (description == null) { + setDescription(new StringFilter()); + } + return description; + } + + public void setDescription(StringFilter description) { + this.description = description; + } + + public LongFilter getFromUnitId() { + return fromUnitId; + } + + public Optional optionalFromUnitId() { + return Optional.ofNullable(fromUnitId); + } + + public LongFilter fromUnitId() { + if (fromUnitId == null) { + setFromUnitId(new LongFilter()); + } + return fromUnitId; + } + + public void setFromUnitId(LongFilter fromUnitId) { + this.fromUnitId = fromUnitId; + } + + public LongFilter getToUnitId() { + return toUnitId; + } + + public Optional optionalToUnitId() { + return Optional.ofNullable(toUnitId); + } + + public LongFilter toUnitId() { + if (toUnitId == null) { + setToUnitId(new LongFilter()); + } + return toUnitId; + } + + public void setToUnitId(LongFilter toUnitId) { + this.toUnitId = toUnitId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final UnitConverterCriteria that = (UnitConverterCriteria) o; + return (Objects.equals(id, that.id) && Objects.equals(name, that.name) + && Objects.equals(description, that.description) && Objects.equals(fromUnitId, that.fromUnitId) + && Objects.equals(toUnitId, that.toUnitId) && super.equals(o)); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, description, fromUnitId, toUnitId, super.hashCode()); + } + + // prettier-ignore + @Override + public String toString() { + return "InputDataCriteria{" + optionalId().map(f -> "id=" + f + ", ").orElse("") + + optionalName().map(f -> "name=" + f + ", ").orElse("") + + optionalDescription().map(f -> "description=" + f + ", ").orElse("") + + optionalFromUnitId().map(f -> "fromUnitId=" + f + ", ").orElse("") + + optionalToUnitId().map(f -> "toUnitId=" + f + ", ").orElse("") + super.toString() + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/UnitCriteria.java b/src/main/java/com/oguerreiro/resilient/service/criteria/UnitCriteria.java new file mode 100644 index 0000000..57af8df --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/UnitCriteria.java @@ -0,0 +1,200 @@ +package com.oguerreiro.resilient.service.criteria; + +import java.util.Objects; +import java.util.Optional; + +import org.springdoc.core.annotations.ParameterObject; + +import tech.jhipster.service.filter.Filter; +import tech.jhipster.service.filter.LongFilter; +import tech.jhipster.service.filter.StringFilter; + +/** + * Criteria class for the {@link com.oguerreiro.resilient.domain.Unit} + * entity. This class is used in + * {@link com.oguerreiro.resilient.web.rest.UnitResource} to receive all + * the possible filtering options from the Http GET request parameters. For + * example the following could be a valid request: + * {@code /unit?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + * As Spring is unable to properly convert the types, unless specific + * {@link Filter} class are used, we need to use fix type specific filters. + */ +@ParameterObject +public class UnitCriteria extends AbstractCriteria { + private static final long serialVersionUID = 1L; + + private LongFilter id; + + private StringFilter code; + + private StringFilter name; + + private StringFilter symbol; + + private StringFilter description; + + private LongFilter unitTypeId; + + public UnitCriteria() { + } + + public UnitCriteria(UnitCriteria other) { + super(other); + + this.id = other.optionalId().map(LongFilter::copy).orElse(null); + this.code = other.optionalCode().map(StringFilter::copy).orElse(null); + this.name = other.optionalName().map(StringFilter::copy).orElse(null); + this.symbol = other.optionalSymbol().map(StringFilter::copy).orElse(null); + this.description = other.optionalDescription().map(StringFilter::copy).orElse(null); + this.unitTypeId = other.optionalUnitTypeId().map(LongFilter::copy).orElse(null); + } + + @Override + public UnitCriteria copy() { + return new UnitCriteria(this); + } + + public LongFilter getId() { + return id; + } + + public Optional optionalId() { + return Optional.ofNullable(id); + } + + public LongFilter id() { + if (id == null) { + setId(new LongFilter()); + } + return id; + } + + public void setId(LongFilter id) { + this.id = id; + } + + public StringFilter getCode() { + return code; + } + + public Optional optionalCode() { + return Optional.ofNullable(code); + } + + public StringFilter code() { + if (code == null) { + setCode(new StringFilter()); + } + return code; + } + + public void setCode(StringFilter code) { + this.code = code; + } + + public StringFilter getName() { + return name; + } + + public Optional optionalName() { + return Optional.ofNullable(name); + } + + public StringFilter name() { + if (name == null) { + setName(new StringFilter()); + } + return name; + } + + public void setName(StringFilter name) { + this.name = name; + } + + public StringFilter getDescription() { + return description; + } + + public StringFilter getSymbol() { + return symbol; + } + + public Optional optionalSymbol() { + return Optional.ofNullable(symbol); + } + + public StringFilter symbol() { + if (symbol == null) { + setSymbol(new StringFilter()); + } + return symbol; + } + + public void setSymbol(StringFilter symbol) { + this.symbol = symbol; + } + + public Optional optionalDescription() { + return Optional.ofNullable(description); + } + + public StringFilter description() { + if (description == null) { + setDescription(new StringFilter()); + } + return description; + } + + public void setDescription(StringFilter description) { + this.description = description; + } + + public LongFilter getUnitTypeId() { + return unitTypeId; + } + + public Optional optionalUnitTypeId() { + return Optional.ofNullable(unitTypeId); + } + + public LongFilter unitTypeId() { + if (unitTypeId == null) { + setUnitTypeId(new LongFilter()); + } + return unitTypeId; + } + + public void setUnitTypeId(LongFilter unitTypeId) { + this.unitTypeId = unitTypeId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final UnitCriteria that = (UnitCriteria) o; + return (Objects.equals(id, that.id) && Objects.equals(code, that.code) && Objects.equals(name, that.name) + && Objects.equals(symbol, that.symbol) && Objects.equals(description, that.description) + && Objects.equals(unitTypeId, that.unitTypeId) && super.equals(o)); + } + + @Override + public int hashCode() { + return Objects.hash(id, code, name, symbol, description, unitTypeId, super.hashCode()); + } + + // prettier-ignore + @Override + public String toString() { + return "UnitCriteria{" + optionalId().map(f -> "id=" + f + ", ").orElse("") + + optionalCode().map(f -> "code=" + f + ", ").orElse("") + + optionalName().map(f -> "name=" + f + ", ").orElse("") + + optionalSymbol().map(f -> "symbol=" + f + ", ").orElse("") + + optionalDescription().map(f -> "description=" + f + ", ").orElse("") + + optionalUnitTypeId().map(f -> "unitTypeId=" + f + ", ").orElse("") + super.toString() + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/VariableCriteria.java b/src/main/java/com/oguerreiro/resilient/service/criteria/VariableCriteria.java new file mode 100644 index 0000000..2dfe03b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/VariableCriteria.java @@ -0,0 +1,421 @@ +package com.oguerreiro.resilient.service.criteria; + +import java.util.Objects; +import java.util.Optional; + +import org.springdoc.core.annotations.ParameterObject; + +import com.oguerreiro.resilient.domain.enumeration.InputMode; + +import tech.jhipster.service.filter.BooleanFilter; +import tech.jhipster.service.filter.Filter; +import tech.jhipster.service.filter.IntegerFilter; +import tech.jhipster.service.filter.LongFilter; +import tech.jhipster.service.filter.StringFilter; + +/** + * Criteria class for the {@link com.oguerreiro.resilient.domain.Variable} entity. This class is used + * in {@link com.oguerreiro.resilient.web.rest.VariableResource} to receive all the possible filtering options from + * the Http GET request parameters. + * For example the following could be a valid request: + * {@code /variables?id.greaterThan=5&attr1.contains=something&attr2.specified=false} + * As Spring is unable to properly convert the types, unless specific {@link Filter} class are used, we need to use + * fix type specific filters. + */ +@ParameterObject +public class VariableCriteria extends AbstractCriteria { + + /** + * Class for filtering InputMode + */ + public static class InputModeFilter extends Filter { + private static final long serialVersionUID = 1L; + + public InputModeFilter() { + } + + public InputModeFilter(InputModeFilter filter) { + super(filter); + } + + @Override + public InputModeFilter copy() { + return new InputModeFilter(this); + } + } + + private static final long serialVersionUID = 1L; + + private LongFilter id; + + private StringFilter code; + + private IntegerFilter variableIndex; + + private StringFilter name; + + private StringFilter description; + + private BooleanFilter input; + + private InputModeFilter inputMode; + + private BooleanFilter output; + + private StringFilter outputFormula; + + private IntegerFilter version; + + private LongFilter variableClassId; + + private LongFilter variableUnitsId; + + private LongFilter baseUnitId; + + private LongFilter variableScopeId; + + private LongFilter variableCategoryId; + + public VariableCriteria() { + super(); + } + + public VariableCriteria(VariableCriteria other) { + super(other); + + this.id = other.optionalId().map(LongFilter::copy).orElse(null); + this.code = other.optionalCode().map(StringFilter::copy).orElse(null); + this.variableIndex = other.optionalVariableIndex().map(IntegerFilter::copy).orElse(null); + this.name = other.optionalName().map(StringFilter::copy).orElse(null); + this.description = other.optionalDescription().map(StringFilter::copy).orElse(null); + this.input = other.optionalInput().map(BooleanFilter::copy).orElse(null); + this.inputMode = other.optionalInputMode().map(InputModeFilter::copy).orElse(null); + this.output = other.optionalOutput().map(BooleanFilter::copy).orElse(null); + this.outputFormula = other.optionalOutputFormula().map(StringFilter::copy).orElse(null); + this.version = other.optionalVersion().map(IntegerFilter::copy).orElse(null); + this.variableUnitsId = other.optionalVariableUnitsId().map(LongFilter::copy).orElse(null); + this.baseUnitId = other.optionalBaseUnitId().map(LongFilter::copy).orElse(null); + this.variableScopeId = other.optionalVariableScopeId().map(LongFilter::copy).orElse(null); + this.variableCategoryId = other.optionalVariableCategoryId().map(LongFilter::copy).orElse(null); + } + + @Override + public VariableCriteria copy() { + return new VariableCriteria(this); + } + + public LongFilter getId() { + return id; + } + + public Optional optionalId() { + return Optional.ofNullable(id); + } + + public LongFilter id() { + if (id == null) { + setId(new LongFilter()); + } + return id; + } + + public void setId(LongFilter id) { + this.id = id; + } + + public StringFilter getCode() { + return code; + } + + public Optional optionalCode() { + return Optional.ofNullable(code); + } + + public StringFilter code() { + if (code == null) { + setCode(new StringFilter()); + } + return code; + } + + public void setCode(StringFilter code) { + this.code = code; + } + + public IntegerFilter getVariableIndex() { + return variableIndex; + } + + public Optional optionalVariableIndex() { + return Optional.ofNullable(variableIndex); + } + + public IntegerFilter variableIndex() { + if (variableIndex == null) { + setVariableIndex(new IntegerFilter()); + } + return variableIndex; + } + + public void setVariableIndex(IntegerFilter variableIndex) { + this.variableIndex = variableIndex; + } + + public StringFilter getName() { + return name; + } + + public Optional optionalName() { + return Optional.ofNullable(name); + } + + public StringFilter name() { + if (name == null) { + setName(new StringFilter()); + } + return name; + } + + public void setName(StringFilter name) { + this.name = name; + } + + public StringFilter getDescription() { + return description; + } + + public Optional optionalDescription() { + return Optional.ofNullable(description); + } + + public StringFilter description() { + if (description == null) { + setDescription(new StringFilter()); + } + return description; + } + + public void setDescription(StringFilter description) { + this.description = description; + } + + public BooleanFilter getInput() { + return input; + } + + public Optional optionalInput() { + return Optional.ofNullable(input); + } + + public BooleanFilter input() { + if (input == null) { + setInput(new BooleanFilter()); + } + return input; + } + + public void setInput(BooleanFilter input) { + this.input = input; + } + + public InputModeFilter getInputMode() { + return inputMode; + } + + public Optional optionalInputMode() { + return Optional.ofNullable(inputMode); + } + + public InputModeFilter inputMode() { + if (inputMode == null) { + setInputMode(new InputModeFilter()); + } + return inputMode; + } + + public void setInputMode(InputModeFilter inputMode) { + this.inputMode = inputMode; + } + + public BooleanFilter getOutput() { + return output; + } + + public Optional optionalOutput() { + return Optional.ofNullable(output); + } + + public BooleanFilter output() { + if (output == null) { + setOutput(new BooleanFilter()); + } + return output; + } + + public void setOutput(BooleanFilter output) { + this.output = output; + } + + public StringFilter getOutputFormula() { + return outputFormula; + } + + public Optional optionalOutputFormula() { + return Optional.ofNullable(outputFormula); + } + + public StringFilter outputFormula() { + if (outputFormula == null) { + setOutputFormula(new StringFilter()); + } + return outputFormula; + } + + public void setOutputFormula(StringFilter outputFormula) { + this.outputFormula = outputFormula; + } + + public IntegerFilter getVersion() { + return version; + } + + public Optional optionalVersion() { + return Optional.ofNullable(version); + } + + public IntegerFilter version() { + if (version == null) { + setVersion(new IntegerFilter()); + } + return version; + } + + public void setVersion(IntegerFilter version) { + this.version = version; + } + + public void setVariableClassId(LongFilter variableClassId) { + this.variableClassId = variableClassId; + } + + public LongFilter getVariableUnitsId() { + return variableUnitsId; + } + + public Optional optionalVariableUnitsId() { + return Optional.ofNullable(variableUnitsId); + } + + public LongFilter variableUnitsId() { + if (variableUnitsId == null) { + setVariableUnitsId(new LongFilter()); + } + return variableUnitsId; + } + + public void setVariableUnitsId(LongFilter variableUnitsId) { + this.variableUnitsId = variableUnitsId; + } + + public LongFilter getBaseUnitId() { + return baseUnitId; + } + + public Optional optionalBaseUnitId() { + return Optional.ofNullable(baseUnitId); + } + + public LongFilter baseUnitId() { + if (baseUnitId == null) { + setBaseUnitId(new LongFilter()); + } + return baseUnitId; + } + + public void setBaseUnitId(LongFilter baseUnitId) { + this.baseUnitId = baseUnitId; + } + + public LongFilter getVariableScopeId() { + return variableScopeId; + } + + public Optional optionalVariableScopeId() { + return Optional.ofNullable(variableScopeId); + } + + public LongFilter variableScopeId() { + if (variableScopeId == null) { + setVariableScopeId(new LongFilter()); + } + return variableScopeId; + } + + public void setVariableScopeId(LongFilter variableScopeId) { + this.variableScopeId = variableScopeId; + } + + public LongFilter getVariableCategoryId() { + return variableCategoryId; + } + + public Optional optionalVariableCategoryId() { + return Optional.ofNullable(variableCategoryId); + } + + public LongFilter variableCategoryId() { + if (variableCategoryId == null) { + setVariableCategoryId(new LongFilter()); + } + return variableCategoryId; + } + + public void setVariableCategoryId(LongFilter variableCategoryId) { + this.variableCategoryId = variableCategoryId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final VariableCriteria that = (VariableCriteria) o; + return (Objects.equals(id, that.id) && Objects.equals(code, that.code) + && Objects.equals(variableIndex, that.variableIndex) && Objects.equals(name, that.name) + && Objects.equals(description, that.description) && Objects.equals(input, that.input) + && Objects.equals(inputMode, that.inputMode) && Objects.equals(output, that.output) + && Objects.equals(outputFormula, that.outputFormula) && Objects.equals(version, that.version) + && Objects.equals(variableClassId, that.variableClassId) + && Objects.equals(variableUnitsId, that.variableUnitsId) && Objects.equals(baseUnitId, that.baseUnitId) + && Objects.equals(variableScopeId, that.variableScopeId) + && Objects.equals(variableCategoryId, that.variableCategoryId) && super.equals(o)); + } + + @Override + public int hashCode() { + return Objects.hash(id, code, variableIndex, name, description, input, inputMode, output, outputFormula, version, + variableClassId, variableUnitsId, baseUnitId, variableScopeId, variableCategoryId, super.hashCode()); + } + + // prettier-ignore + @Override + public String toString() { + return "VariableCriteria{" + optionalId().map(f -> "id=" + f + ", ").orElse("") + + optionalCode().map(f -> "code=" + f + ", ").orElse("") + + optionalVariableIndex().map(f -> "variableIndex=" + f + ", ").orElse("") + + optionalName().map(f -> "name=" + f + ", ").orElse("") + + optionalDescription().map(f -> "description=" + f + ", ").orElse("") + + optionalInput().map(f -> "input=" + f + ", ").orElse("") + + optionalInputMode().map(f -> "inputMode=" + f + ", ").orElse("") + + optionalOutput().map(f -> "output=" + f + ", ").orElse("") + + optionalOutputFormula().map(f -> "outputFormula=" + f + ", ").orElse("") + + optionalVersion().map(f -> "version=" + f + ", ").orElse("") + + optionalVariableUnitsId().map(f -> "variableUnitsId=" + f + ", ").orElse("") + + optionalBaseUnitId().map(f -> "baseUnitId=" + f + ", ").orElse("") + + optionalVariableScopeId().map(f -> "variableScopeId=" + f + ", ").orElse("") + + optionalVariableCategoryId().map(f -> "variableCategoryId=" + f + ", ").orElse("") + super.toString() + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/criteria/package-info.java b/src/main/java/com/oguerreiro/resilient/service/criteria/package-info.java new file mode 100644 index 0000000..685d06a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/criteria/package-info.java @@ -0,0 +1,4 @@ +/** + * This package file was generated by JHipster + */ +package com.oguerreiro.resilient.service.criteria; diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/AbstractResilientDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/AbstractResilientDTO.java new file mode 100644 index 0000000..1e45af5 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/AbstractResilientDTO.java @@ -0,0 +1,58 @@ +package com.oguerreiro.resilient.service.dto; + +import java.io.Serializable; +import java.util.Objects; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Base Resilient DTO. All DTO's must extend this class. + * + * @param - the class type of the entity domain primary key + */ +public abstract class AbstractResilientDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private ID id; + + @Schema(description = "Record version for concurrency control.") + private Integer version; + + public ID getId() { + return id; + } + + public void setId(ID id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!o.getClass().isInstance(this.getClass())) { + return false; + } + + AbstractResilientDTO domainDTO = (AbstractResilientDTO) o; + if (this.id == null) { + return false; + } + return Objects.equals(this.id, domainDTO.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/ActivityDomainDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/ActivityDomainDTO.java new file mode 100644 index 0000000..ef56d7d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/ActivityDomainDTO.java @@ -0,0 +1,32 @@ +package com.oguerreiro.resilient.service.dto; + +import java.io.Serializable; + +import io.swagger.v3.oas.annotations.media.Schema; + +public abstract class ActivityDomainDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @Schema(description = "The class name of the Entity domain class. Mandatory for Activity enabled entities.") + private String activityDomainClass; + + @Schema(description = "The key of the ActivityProgress, when an activity is currntly in execution.") + private String activityProgressKey; + + public String getActivityDomainClass() { + return activityDomainClass; + } + + public void setActivityDomainClass(String activityDomainClass) { + this.activityDomainClass = activityDomainClass; + } + + public String getActivityProgressKey() { + return activityProgressKey; + } + + public void setActivityProgressKey(String activityProgressKey) { + this.activityProgressKey = activityProgressKey; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/AdminUserDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/AdminUserDTO.java new file mode 100644 index 0000000..7b2248b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/AdminUserDTO.java @@ -0,0 +1,233 @@ +package com.oguerreiro.resilient.service.dto; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.security.core.GrantedAuthority; + +import com.oguerreiro.resilient.config.Constants; +import com.oguerreiro.resilient.domain.Authority; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.security.custom.ResilientUserDetails; +import com.oguerreiro.resilient.service.dto.security.SecurityGroupDTO; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +/** + * A DTO representing a user, with his authorities. + */ +public class AdminUserDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + @NotBlank + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + private String login; + + @Size(max = 50) + private String firstName; + + @Size(max = 50) + private String lastName; + + @Email + @Size(min = 5, max = 254) + private String email; + + @Size(max = 256) + private String imageUrl; + + private boolean activated = false; + + @Size(min = 2, max = 10) + private String langKey; + + private String createdBy; + + private Instant createdDate; + + private String lastModifiedBy; + + private Instant lastModifiedDate; + + private Set authorities; + + private SecurityGroupDTO securityGroup; + + private OrganizationDTO parentOrganization; + + public AdminUserDTO() { + // Empty constructor needed for Jackson. + } + + public AdminUserDTO(User user) { + this.id = user.getId(); + this.login = user.getLogin(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.email = user.getEmail(); + this.activated = user.isActivated(); + this.imageUrl = user.getImageUrl(); + this.langKey = user.getLangKey(); + this.createdBy = user.getCreatedBy(); + this.createdDate = user.getCreatedDate(); + this.lastModifiedBy = user.getLastModifiedBy(); + this.lastModifiedDate = user.getLastModifiedDate(); + this.authorities = user.getAuthorities().stream().map(Authority::getName).collect(Collectors.toSet()); + this.securityGroup = new SecurityGroupDTO(user.getSecurityGroup()); + } + + public AdminUserDTO(ResilientUserDetails user) { + this.login = user.getUsername(); + this.authorities = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); + this.securityGroup = new SecurityGroupDTO(user.getSecurityGroup()); + + Organization userOrg = user.getParentOrganization(); + if (userOrg != null) { + this.parentOrganization = new OrganizationDTO(); + this.parentOrganization.setCode(userOrg.getCode()); + this.parentOrganization.setId(userOrg.getId()); + this.parentOrganization.setName(userOrg.getName()); + } + this.langKey = user.getLangKey(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getLangKey() { + return langKey; + } + + public void setLangKey(String langKey) { + this.langKey = langKey; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Instant createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public Instant getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(Instant lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + public SecurityGroupDTO getSecurityGroup() { + return this.securityGroup; + } + + public void setSecurityGroup(SecurityGroupDTO securityGroup) { + this.securityGroup = securityGroup; + } + + public OrganizationDTO getParentOrganization() { + return parentOrganization; + } + + public void setParentOrganization(OrganizationDTO parentOrganization) { + this.parentOrganization = parentOrganization; + } + + // prettier-ignore + @Override + public String toString() { + return "AdminUserDTO{" + "login='" + login + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + + '\'' + ", email='" + email + '\'' + ", imageUrl='" + imageUrl + '\'' + ", activated=" + activated + + ", langKey='" + langKey + '\'' + ", createdBy=" + createdBy + ", createdDate=" + createdDate + + ", lastModifiedBy='" + lastModifiedBy + '\'' + ", lastModifiedDate=" + lastModifiedDate + ", authorities=" + + authorities + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/ContentPageDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/ContentPageDTO.java new file mode 100644 index 0000000..ac8f5c7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/ContentPageDTO.java @@ -0,0 +1,54 @@ +package com.oguerreiro.resilient.service.dto; + +import com.oguerreiro.resilient.domain.enumeration.ContentPageType; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.ContentPage} entity. + */ +@Schema(description = "ContentPage") +public class ContentPageDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + private String title; + private ContentPageType slug; + private String content; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public ContentPageType getSlug() { + return slug; + } + + public void setSlug(ContentPageType slug) { + this.slug = slug; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + //@formatter:off + @Override + public String toString() { + return "DashboardComponentDTO{" + + "id=" + getId() + + ", title='" + getTitle() + "'" + + ", slug='" + getSlug() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDTO.java new file mode 100644 index 0000000..592a725 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDTO.java @@ -0,0 +1,145 @@ +package com.oguerreiro.resilient.service.dto; + +import java.util.ArrayList; +import java.util.List; + +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentChartType; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentType; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.DashboardComponent} entity. + */ +@Schema(description = "DashboardComponent") +public class DashboardComponentDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Required and unique code for the component", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Schema(description = "Name of the component.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + private String description; + + private Boolean isActive; + + @NotNull + private Boolean isExportable; + + @NotNull + private DashboardComponentType type; + + private DashboardComponentView view; + + private DashboardComponentChartType chartType; + + private List dashboardComponentDetails = new ArrayList<>(); + + private List dashboardComponentOrganizations = new ArrayList<>(); + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public DashboardComponentType getType() { + return type; + } + + public void setType(DashboardComponentType type) { + this.type = type; + } + + public DashboardComponentView getView() { + return view; + } + + public void setView(DashboardComponentView view) { + this.view = view; + } + + public DashboardComponentChartType getChartType() { + return chartType; + } + + public void setChartType(DashboardComponentChartType chartType) { + this.chartType = chartType; + } + + public List getDashboardComponentDetails() { + return dashboardComponentDetails; + } + + public void setDashboardComponentDetails(List dashboardComponentDetails) { + this.dashboardComponentDetails = dashboardComponentDetails; + } + + public List getDashboardComponentOrganizations() { + return dashboardComponentOrganizations; + } + + public void setDashboardComponentOrganizations( + List dashboardComponentOrganizations) { + this.dashboardComponentOrganizations = dashboardComponentOrganizations; + } + + public Boolean getIsExportable() { + return isExportable; + } + + public void setIsExportable(Boolean isExportable) { + this.isExportable = isExportable; + } + + //@formatter:off + @Override + public String toString() { + return "DashboardComponentDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", type='" + getType() + "'" + + ", view='" + getView() + "'" + + ", chartType='" + getChartType() + "'" + + ", isExportable='" + getIsExportable() + "'" + + ", isActive='" + getIsActive() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDetailDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDetailDTO.java new file mode 100644 index 0000000..83a62c8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDetailDTO.java @@ -0,0 +1,101 @@ +package com.oguerreiro.resilient.service.dto; + +import com.oguerreiro.resilient.domain.DashboardComponent; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.DashboardComponent} entity. + */ +@Schema(description = "DashboardComponentDetail") +public class DashboardComponentDetailDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + @NotNull + private String name; + + private String color; + + private String help; + + @NotNull + private Integer indexOrder; + + private VariableDTO variable; + + private DashboardComponentDTO dashboardComponent; + + private DashboardComponent targetDashboardComponent; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public String getHelp() { + return help; + } + + public void setHelp(String help) { + this.help = help; + } + + public Integer getIndexOrder() { + return indexOrder; + } + + public void setIndexOrder(Integer indexOrder) { + this.indexOrder = indexOrder; + } + + public VariableDTO getVariable() { + return variable; + } + + public void setVariable(VariableDTO variable) { + this.variable = variable; + } + + public DashboardComponentDTO getDashboardComponent() { + return dashboardComponent; + } + + public void setDashboardComponent(DashboardComponentDTO dashboardComponent) { + this.dashboardComponent = dashboardComponent; + } + + public DashboardComponent getTargetDashboardComponent() { + return targetDashboardComponent; + } + + public void setTargetDashboardComponent(DashboardComponent targetDashboardComponent) { + this.targetDashboardComponent = targetDashboardComponent; + } + + //@formatter:off + @Override + public String toString() { + return "DashboardComponentDetailDTO{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", help='" + getHelp() + "'" + + ", color='" + getColor() + "'" + + ", indexOrder='" + getIndexOrder() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDetailValueDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDetailValueDTO.java new file mode 100644 index 0000000..371fbb9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentDetailValueDTO.java @@ -0,0 +1,62 @@ +package com.oguerreiro.resilient.service.dto; + +import java.io.Serializable; +import java.math.BigDecimal; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.DashboardComponentDetailValue} entity. + */ +@Schema(description = "DashboardComponentDetailValue") +public class DashboardComponentDetailValueDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private DashboardComponentDetailDTO dashboardComponentDetail; + private DashboardComponentDTO targetDashboardComponent; + private OrganizationDTO organization; + private BigDecimal value; + + public DashboardComponentDetailDTO getDashboardComponentDetail() { + return this.dashboardComponentDetail; + } + + public void setDashboardComponentDetail(DashboardComponentDetailDTO dashboardComponentDetail) { + this.dashboardComponentDetail = dashboardComponentDetail; + } + + public OrganizationDTO getOrganization() { + return this.organization; + } + + public void setOrganization(OrganizationDTO organization) { + this.organization = organization; + } + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public DashboardComponentDTO getTargetDashboardComponent() { + return targetDashboardComponent; + } + + public void setTargetDashboardComponent(DashboardComponentDTO targetDashboardComponent) { + this.targetDashboardComponent = targetDashboardComponent; + } + + //@formatter:off + @Override + public String toString() { + return "DashboardComponentViewDetailDTO{" + + "organizationId=" + getOrganization().getId() + + ", value=" + getValue().toString() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentOrganizationDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentOrganizationDTO.java new file mode 100644 index 0000000..b64284b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentOrganizationDTO.java @@ -0,0 +1,45 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.DashboardComponentOrganization} entity. + */ +@Schema(description = "DashboardComponentOrganization") +public class DashboardComponentOrganizationDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + @NotNull + private DashboardComponentDTO dashboardComponent; + + @NotNull + private OrganizationDTO organization; + + public DashboardComponentDTO getDashboardComponent() { + return dashboardComponent; + } + + public void setDashboardComponent(DashboardComponentDTO dashboardComponent) { + this.dashboardComponent = dashboardComponent; + } + + public OrganizationDTO getOrganization() { + return organization; + } + + public void setOrganization(OrganizationDTO organization) { + this.organization = organization; + } + + //@formatter:off + @Override + public String toString() { + return "DashboardComponentOrganizationDTO{" + + "id=" + getId() + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentViewDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentViewDTO.java new file mode 100644 index 0000000..a088646 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardComponentViewDTO.java @@ -0,0 +1,33 @@ +package com.oguerreiro.resilient.service.dto; + +import java.util.ArrayList; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.DashboardComponent} entity. + */ +@Schema(description = "DashboardComponentView") +public class DashboardComponentViewDTO extends DashboardComponentDTO { + + private static final long serialVersionUID = 1L; + + private List dashboardComponentViewDetails = new ArrayList<>(); + + public List getDashboardComponentViewDetails() { + return dashboardComponentViewDetails; + } + + public void setDashboardComponentViewDetails(List dashboardComponentViewDetails) { + this.dashboardComponentViewDetails = dashboardComponentViewDetails; + } + +//@formatter:off + @Override + public String toString() { + return "DashboardComponentViewDTO{" + super.toString() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/DashboardDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardDTO.java new file mode 100644 index 0000000..b71818b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/DashboardDTO.java @@ -0,0 +1,71 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.Dashboard} entity. + */ +@Schema(description = "Dashboard") +public class DashboardDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Required and unique code for the component", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Schema(description = "Name of the component.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + private String description; + + private Boolean isInternal; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getIsInternal() { + return isInternal; + } + + public void setIsInternal(Boolean isInternal) { + this.isInternal = isInternal; + } + +//@formatter:off + @Override + public String toString() { + return "DashboardComponentDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", isInternal='" + getIsInternal() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/DocumentDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/DocumentDTO.java new file mode 100644 index 0000000..60b07a3 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/DocumentDTO.java @@ -0,0 +1,71 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Lob; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.Document} entity. + */ +@Schema(description = "Document with attach") +public class DocumentDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "A simple and small text with a title, for human reference and better relation", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + + @Schema(description = "An additional optional description") + private String description; + + @Schema(description = "An Excel file, that must match a template for input", requiredMode = Schema.RequiredMode.REQUIRED) + @Lob + private byte[] dataFile; + + private String dataFileContentType; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public byte[] getDataFile() { + return dataFile; + } + + public void setDataFile(byte[] dataFile) { + this.dataFile = dataFile; + } + + public String getDataFileContentType() { + return dataFileContentType; + } + + public void setDataFileContentType(String dataFileContentType) { + this.dataFileContentType = dataFileContentType; + } + +//@formatter:off + @Override + public String toString() { + return "InputDataUploadDTO{" + + "id=" + getId() + + ", title='" + getTitle() + "'" + + ", description='" + getDescription() + "'" + + ", dataFile='" + getDataFile() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/EmissionFactorDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/EmissionFactorDTO.java new file mode 100644 index 0000000..3a4bb9f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/EmissionFactorDTO.java @@ -0,0 +1,137 @@ +package com.oguerreiro.resilient.service.dto; + +import java.math.BigDecimal; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.EmissionFactor} entity. + */ +@Schema(description = "Emission Factor") +public class EmissionFactorDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Year that the factor belongs to", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer year; + + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Schema(description = "Unique key, within the year", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Schema(description = "The name or title for the Emission factor", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @NotNull + @Schema(description = "The unit of the Emission Factor.", requiredMode = Schema.RequiredMode.REQUIRED) + private String unit; + + @NotNull + @Schema(description = "The value of the factor.", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal value; + + @Schema(description = "The source of the Emission Factor.") + private String source; + + @Schema(description = "Comments") + private String comments; + + private VariableScopeDTO variableScope; + + private VariableCategoryDTO variableCategory; + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public VariableScopeDTO getVariableScope() { + return variableScope; + } + + public void setVariableScope(VariableScopeDTO variableScope) { + this.variableScope = variableScope; + } + + public VariableCategoryDTO getVariableCategory() { + return variableCategory; + } + + public void setVariableCategory(VariableCategoryDTO variableCategory) { + this.variableCategory = variableCategory; + } + +//@formatter:off + @Override + public String toString() { + return "EmissionFactorDTO{" + + "id=" + getId() + + ", year='" + getYear() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", unit='" + getUnit() + "'" + + ", value=" + getValue() + + ", source='" + getSource() + "'" + + ", comments='" + getComments() + "'" + + ", variableScope=" + getVariableScope() + + ", variableCategory=" + getVariableCategory() + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/InputDataDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/InputDataDTO.java new file mode 100644 index 0000000..231c47e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/InputDataDTO.java @@ -0,0 +1,294 @@ +package com.oguerreiro.resilient.service.dto; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; + +import com.oguerreiro.resilient.domain.enumeration.DataSourceType; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.InputData} entity. + */ +@Schema(description = "Input data Values\nThe values inserted from:\n- Each Organic Unit\n- Variable\n- PeriodVersion") +public class InputDataDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + private String stringValue; + + @Schema(description = "The value of the data in the source Unit. This might be different from the Variable baseUnit. @see variableValue and variableUnit", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal sourceValue; + + @Schema(description = "The value of the data converted to the variable baseUnit.", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal variableValue; + + @Schema(description = "The value that the InputData.owner is accounted for.\nIn case a variableValue is distributed to other Organization's (InputData.source==DataSources.DISTRIB)\n, this will have the value to consider when calculating the final inventory value for the variable", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal imputedValue; + + @NotNull + @Schema(description = "What was the source of this data ?", requiredMode = Schema.RequiredMode.REQUIRED) + private DataSourceType sourceType; + + @Schema(description = "The date that this data relates to.\nEg. energy consumptium happens every month, this should be the date of invoice\n[optional]") + private LocalDate dataDate; + + @NotNull + @Schema(description = "Last change date", requiredMode = Schema.RequiredMode.REQUIRED) + private Instant changeDate; + + @NotNull + @Schema(description = "The actual username logged into the system, that last changed the data", requiredMode = Schema.RequiredMode.REQUIRED) + private String changeUsername; + + @Schema(description = "Where this data originates from? Where were the data collected ?") + private String dataSource; + + @Schema(description = "Who collected the data ?") + private String dataUser; + + @Schema(description = "Comments added by the user") + private String dataComments; + + @NotNull + @Schema(description = "The creation date (insert). When the data was inserted into the system.", requiredMode = Schema.RequiredMode.REQUIRED) + private Instant creationDate; + + @NotNull + @Schema(description = "The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username.", requiredMode = Schema.RequiredMode.REQUIRED) + private String creationUsername; + + private String variableClassCode; + private String variableClassName; + + private VariableDTO variable; + + private PeriodDTO period; + + private PeriodVersionDTO periodVersion; + + private UnitDTO sourceUnit; + + private UnitDTO unit; + + private OrganizationDTO owner; + + private InputDataDTO sourceInputData; + + private InputDataUploadDTO sourceInputDataUpload; + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public BigDecimal getSourceValue() { + return sourceValue; + } + + public void setSourceValue(BigDecimal sourceValue) { + this.sourceValue = sourceValue; + } + + public BigDecimal getVariableValue() { + return variableValue; + } + + public void setVariableValue(BigDecimal variableValue) { + this.variableValue = variableValue; + } + + public BigDecimal getImputedValue() { + return imputedValue; + } + + public void setImputedValue(BigDecimal imputedValue) { + this.imputedValue = imputedValue; + } + + public DataSourceType getSourceType() { + return sourceType; + } + + public void setSourceType(DataSourceType sourceType) { + this.sourceType = sourceType; + } + + public LocalDate getDataDate() { + return dataDate; + } + + public void setDataDate(LocalDate dataDate) { + this.dataDate = dataDate; + } + + public Instant getChangeDate() { + return changeDate; + } + + public void setChangeDate(Instant changeDate) { + this.changeDate = changeDate; + } + + public String getChangeUsername() { + return changeUsername; + } + + public void setChangeUsername(String changeUsername) { + this.changeUsername = changeUsername; + } + + public String getDataSource() { + return dataSource; + } + + public void setDataSource(String dataSource) { + this.dataSource = dataSource; + } + + public String getDataUser() { + return dataUser; + } + + public void setDataUser(String dataUser) { + this.dataUser = dataUser; + } + + public String getDataComments() { + return dataComments; + } + + public void setDataComments(String dataComments) { + this.dataComments = dataComments; + } + + public Instant getCreationDate() { + return creationDate; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return creationUsername; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public String getVariableClassCode() { + return variableClassCode; + } + + public void setVariableClassCode(String variableClassCode) { + this.variableClassCode = variableClassCode; + } + + public String getVariableClassName() { + return variableClassName; + } + + public void setVariableClassName(String variableClassName) { + this.variableClassName = variableClassName; + } + + public VariableDTO getVariable() { + return variable; + } + + public void setVariable(VariableDTO variable) { + this.variable = variable; + } + + public PeriodDTO getPeriod() { + return period; + } + + public void setPeriod(PeriodDTO period) { + this.period = period; + } + + public PeriodVersionDTO getPeriodVersion() { + return periodVersion; + } + + public void setPeriodVersion(PeriodVersionDTO periodVersion) { + this.periodVersion = periodVersion; + } + + public UnitDTO getSourceUnit() { + return sourceUnit; + } + + public void setSourceUnit(UnitDTO sourceUnit) { + this.sourceUnit = sourceUnit; + } + + public UnitDTO getUnit() { + return unit; + } + + public void setUnit(UnitDTO unit) { + this.unit = unit; + } + + public OrganizationDTO getOwner() { + return owner; + } + + public void setOwner(OrganizationDTO owner) { + this.owner = owner; + } + + public InputDataDTO getSourceInputData() { + return sourceInputData; + } + + public void setSourceInputData(InputDataDTO sourceInputData) { + this.sourceInputData = sourceInputData; + } + + public InputDataUploadDTO getSourceInputDataUpload() { + return sourceInputDataUpload; + } + + public void setSourceInputDataUpload(InputDataUploadDTO sourceInputDataUpload) { + this.sourceInputDataUpload = sourceInputDataUpload; + } + +//@formatter:off + @Override + public String toString() { + return "InputDataDTO{" + + "id=" + getId() + + ", sourceValue=" + getSourceValue() + + ", variableValue=" + getVariableValue() + + ", imputedValue=" + getImputedValue() + + ", sourceType='" + getSourceType() + "'" + + ", dataDate='" + getDataDate() + "'" + + ", changeDate='" + getChangeDate() + "'" + + ", changeUsername='" + getChangeUsername() + "'" + + ", dataSource='" + getDataSource() + "'" + + ", dataUser='" + getDataUser() + "'" + + ", dataComments='" + getDataComments() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + ", variable=" + getVariable() + + ", period=" + getPeriod() + + ", periodVersion=" + getPeriodVersion() + + ", sourceUnit=" + getSourceUnit() + + ", unit=" + getUnit() + + ", owner=" + getOwner() + + ", sourceInputData=" + getSourceInputData() + + ", sourceInputDataUpload=" + getSourceInputDataUpload() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/InputDataUploadDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/InputDataUploadDTO.java new file mode 100644 index 0000000..b06b3fe --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/InputDataUploadDTO.java @@ -0,0 +1,182 @@ +package com.oguerreiro.resilient.service.dto; + +import java.time.Instant; + +import com.oguerreiro.resilient.domain.enumeration.UploadStatus; +import com.oguerreiro.resilient.domain.enumeration.UploadType; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Lob; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.InputDataUpload} entity. + */ +@Schema(description = "Input data upload file\nUsers can upload an Excel file with InputData that will automatically insert values in the table InputData") +public class InputDataUploadDTO extends ActivityDomainDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "A simple text with a title, for human reference and better relation", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + + @Schema(description = "An Excel file, that must match a template for input", requiredMode = Schema.RequiredMode.REQUIRED) + @Lob + private byte[] dataFile; + + private String dataFileContentType; + + @Schema(description = "The name of the uploaded Excel file") + private String uploadFileName; + + @Schema(description = "The generated name of the Excel file, used to save to disk") + private String diskFileName; + + @NotNull + @Schema(description = "Type of the Upload", requiredMode = Schema.RequiredMode.REQUIRED) + private UploadType type; + + @Schema(description = "State of the Upload") + private UploadStatus state; + + @Schema(description = "Optional comments") + private String comments; + + @Schema(description = "The creation date (insert). When the data was inserted into the system.") + private Instant creationDate; + + @Schema(description = "The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username.") + private String creationUsername; + + private PeriodDTO period; + + private PeriodVersionDTO periodVersion; + + private OrganizationDTO owner; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public byte[] getDataFile() { + return dataFile; + } + + public void setDataFile(byte[] dataFile) { + this.dataFile = dataFile; + } + + public String getDataFileContentType() { + return dataFileContentType; + } + + public void setDataFileContentType(String dataFileContentType) { + this.dataFileContentType = dataFileContentType; + } + + public String getUploadFileName() { + return uploadFileName; + } + + public void setUploadFileName(String uploadFileName) { + this.uploadFileName = uploadFileName; + } + + public String getDiskFileName() { + return diskFileName; + } + + public void setDiskFileName(String diskFileName) { + this.diskFileName = diskFileName; + } + + public UploadType getType() { + return type; + } + + public void setType(UploadType type) { + this.type = type; + } + + public UploadStatus getState() { + return state; + } + + public void setState(UploadStatus state) { + this.state = state; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public Instant getCreationDate() { + return creationDate; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return creationUsername; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public PeriodDTO getPeriod() { + return period; + } + + public void setPeriod(PeriodDTO period) { + this.period = period; + } + + public PeriodVersionDTO getPeriodVersion() { + return periodVersion; + } + + public void setPeriodVersion(PeriodVersionDTO periodVersion) { + this.periodVersion = periodVersion; + } + + public OrganizationDTO getOwner() { + return owner; + } + + public void setOwner(OrganizationDTO owner) { + this.owner = owner; + } + +//@formatter:off + @Override + public String toString() { + return "InputDataUploadDTO{" + + "id=" + getId() + + ", title='" + getTitle() + "'" + + ", dataFile='" + getDataFile() + "'" + + ", uploadFileName='" + getUploadFileName() + "'" + + ", diskFileName='" + getDiskFileName() + "'" + + ", type='" + getType() + "'" + + ", state='" + getState() + "'" + + ", comments='" + getComments() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", period=" + getPeriod() + + ", periodVersion=" + getPeriodVersion() + + ", owner=" + getOwner() + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/InputDataUploadLogDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/InputDataUploadLogDTO.java new file mode 100644 index 0000000..c496b14 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/InputDataUploadLogDTO.java @@ -0,0 +1,77 @@ +package com.oguerreiro.resilient.service.dto; + +import java.time.Instant; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.InputDataUploadLog} entity. + */ +@Schema(description = "Input data upload file\nUsers can upload an Excel file with InputData that will automatically insert values in the table InputData") +public class InputDataUploadLogDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "The log of the event. Eg. \"InputDataUpload created\"; \"InputDataUpload replaced with new file\"; \"125 records inserted\"; \"User XPTO confirmed the file replacement.\"", requiredMode = Schema.RequiredMode.REQUIRED) + private String logMessage; + + @NotNull + @Schema(description = "The creation date (insert). When the data was inserted into the system.", requiredMode = Schema.RequiredMode.REQUIRED) + private Instant creationDate; + + @NotNull + @Schema(description = "The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username.", requiredMode = Schema.RequiredMode.REQUIRED) + private String creationUsername; + + @Schema(description = "Record version for concurrency control.") + private Integer version; + + private InputDataUploadDTO inputDataUpload; + + public String getLogMessage() { + return logMessage; + } + + public void setLogMessage(String logMessage) { + this.logMessage = logMessage; + } + + public Instant getCreationDate() { + return creationDate; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return creationUsername; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public InputDataUploadDTO getInputDataUpload() { + return inputDataUpload; + } + + public void setInputDataUpload(InputDataUploadDTO inputDataUpload) { + this.inputDataUpload = inputDataUpload; + } + +//@formatter:off + @Override + public String toString() { + return "InputDataUploadLogDTO{" + + "id=" + getId() + + ", logMessage='" + getLogMessage() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", inputDataUpload=" + getInputDataUpload() + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/InventoryDataDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/InventoryDataDTO.java new file mode 100644 index 0000000..35f7655 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/InventoryDataDTO.java @@ -0,0 +1,70 @@ +package com.oguerreiro.resilient.service.dto; + +import java.io.Serializable; +import java.math.BigDecimal; + +public class InventoryDataDTO implements Serializable { + private VariableDTO variable; + private String variableClassCode; + private String variableClassName; + private UnitDTO unit; + private BigDecimal value; + private String stringValue; + + public VariableDTO getVariable() { + return variable; + } + + public void setVariable(VariableDTO variable) { + this.variable = variable; + } + + public String getVariableClassCode() { + return variableClassCode; + } + + public void setVariableClassCode(String variableClassCode) { + this.variableClassCode = variableClassCode; + } + + public String getVariableClassName() { + return variableClassName; + } + + public void setVariableClassName(String variableClassName) { + this.variableClassName = variableClassName; + } + + public UnitDTO getUnit() { + return unit; + } + + public void setUnit(UnitDTO unit) { + this.unit = unit; + } + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + //@formatter:off + @Override + public String toString() { + return "InventoryDataDTO{" + + ", value=" + getValue() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/MetadataPropertyDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/MetadataPropertyDTO.java new file mode 100644 index 0000000..4929b27 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/MetadataPropertyDTO.java @@ -0,0 +1,113 @@ +package com.oguerreiro.resilient.service.dto; + +import java.util.HashSet; +import java.util.Set; + +import com.oguerreiro.resilient.domain.enumeration.MetadataType; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.MetadataProperty} entity. + */ +@Schema(description = "Metadata Property\nConfiguration of a metadata property. Defines its type and a description of its usage.") +public class MetadataPropertyDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Size(min = 3) + private String code; + + @NotNull + private String name; + + private String description; + + private Boolean mandatory; + + @NotNull + private MetadataType metadataType; + + @Schema(description = "Regex pattern for validation (optional). Only applies when the value is not empty.") + private String pattern; + + @Schema(description = "Record version for concurrency control.") + private Integer version; + + private Set organizationTypes = new HashSet<>(); + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getMandatory() { + return mandatory; + } + + public void setMandatory(Boolean mandatory) { + this.mandatory = mandatory; + } + + public MetadataType getMetadataType() { + return metadataType; + } + + public void setMetadataType(MetadataType metadataType) { + this.metadataType = metadataType; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Set getOrganizationTypes() { + return organizationTypes; + } + + public void setOrganizationTypes(Set organizationTypes) { + this.organizationTypes = organizationTypes; + } + + //@formatter:off + @Override + public String toString() { + return "MetadataPropertyDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", mandatory='" + getMandatory() + "'" + + ", metadataType='" + getMetadataType() + "'" + + ", pattern='" + getPattern() + "'" + + ", organizationTypes=" + getOrganizationTypes() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/MetadataValueDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/MetadataValueDTO.java new file mode 100644 index 0000000..3a8703f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/MetadataValueDTO.java @@ -0,0 +1,73 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.MetadataValue} entity. + */ +@Schema(description = "Metadata Value\nThe value of a metadata property.") +public class MetadataValueDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Functional relational key to MetadataProperty", requiredMode = Schema.RequiredMode.REQUIRED) + private String metadataPropertyCode; + + @NotNull + @Schema(description = "The target domain key that this MetadataValue belongs. Its a class name or a key provided by the consumer.
\nUsed with targetDomainId to create as unique relation with the target domain\n@see targetDomainId", requiredMode = Schema.RequiredMode.REQUIRED) + private String targetDomainKey; + + @NotNull + @Schema(description = "The target domain Id that this MetadataValue belongs.
\nUsed with targetDomainKey to create as unique relation with the target domain\n@see targetDomainKey", requiredMode = Schema.RequiredMode.REQUIRED) + private Long targetDomainId; + + @Schema(description = "The value of the MetadataProperty, persisted as a string.
") + private String value; + + public String getMetadataPropertyCode() { + return metadataPropertyCode; + } + + public void setMetadataPropertyCode(String metadataPropertyCode) { + this.metadataPropertyCode = metadataPropertyCode; + } + + public String getTargetDomainKey() { + return targetDomainKey; + } + + public void setTargetDomainKey(String targetDomainKey) { + this.targetDomainKey = targetDomainKey; + } + + public Long getTargetDomainId() { + return targetDomainId; + } + + public void setTargetDomainId(Long targetDomainId) { + this.targetDomainId = targetDomainId; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + //@formatter:off + @Override + public String toString() { + return "MetadataValueDTO{" + + "id=" + getId() + + ", metadataPropertyCode='" + getMetadataPropertyCode() + "'" + + ", targetDomainKey='" + getTargetDomainKey() + "'" + + ", targetDomainId=" + getTargetDomainId() + + ", value='" + getValue() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/OrganizationDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/OrganizationDTO.java new file mode 100644 index 0000000..3e48239 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/OrganizationDTO.java @@ -0,0 +1,140 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Lob; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.Organization} entity. + */ +@Schema(description = "Organization\nHierarchical organigram of the organization.") +public class OrganizationDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Size(min = 3) + @Schema(description = "User defined code. Must be unique in the Organization", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Size(min = 3) + @Schema(description = "The name of the Organization entry. Allows duplicate name's, but NOT duplicate code's.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Lob + private byte[] image; + + private String imageContentType; + + private Boolean inputInventory; + + private Boolean outputInventory; + + private UserDTO user; + + private OrganizationDTO parent; + + private OrganizationTypeDTO organizationType; + + private Integer sort; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public byte[] getImage() { + return image; + } + + public void setImage(byte[] image) { + this.image = image; + } + + public String getImageContentType() { + return imageContentType; + } + + public void setImageContentType(String imageContentType) { + this.imageContentType = imageContentType; + } + + public Boolean getInputInventory() { + return inputInventory; + } + + public void setInputInventory(Boolean inputInventory) { + this.inputInventory = inputInventory; + } + + public Boolean getOutputInventory() { + return outputInventory; + } + + public void setOutputInventory(Boolean outputInventory) { + this.outputInventory = outputInventory; + } + + public UserDTO getUser() { + return user; + } + + public void setUser(UserDTO user) { + this.user = user; + } + + public OrganizationDTO getParent() { + return parent; + } + + public void setParent(OrganizationDTO parent) { + this.parent = parent; + } + + public OrganizationTypeDTO getOrganizationType() { + return organizationType; + } + + public void setOrganizationType(OrganizationTypeDTO organizationType) { + this.organizationType = organizationType; + } + + //@formatter:off + @Override + public String toString() { + return "OrganizationDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", image='" + getImage() + "'" + + ", inputInventory='" + getInputInventory() + "'" + + ", outputInventory='" + getOutputInventory() + "'" + + ", user=" + getUser() + + ", parent=" + getParent() + + ", organizationType=" + getOrganizationType() + + ", sort=" + getSort() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/OrganizationTypeDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/OrganizationTypeDTO.java new file mode 100644 index 0000000..9d4f5cd --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/OrganizationTypeDTO.java @@ -0,0 +1,118 @@ +package com.oguerreiro.resilient.service.dto; + +import java.util.HashSet; +import java.util.Set; + +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Lob; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.OrganizationType} entity. + */ +@Schema(description = "Organization Type\nThe organization type is a setup that defines the usage of the Organization level and other configurations.") +public class OrganizationTypeDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Size(min = 1) + @Schema(description = "User defined code. Must be unique in the OrganizationType", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Size(min = 3) + @Schema(description = "The name of the type. Must be unique in the system", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @NotNull + @Size(min = 3) + @Schema(description = "A more complete description of the type. Whats the meaning of this type?", requiredMode = Schema.RequiredMode.REQUIRED) + private String description; + + @NotNull + @Schema(description = "The nature of the type", requiredMode = Schema.RequiredMode.REQUIRED) + private OrganizationNature nature; + + @Schema(description = "Icon image associated with this organization type. Will be used in user interface.") + @Lob + private byte[] icon; + + private String iconContentType; + + private Set metadataProperties = new HashSet<>(); + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public OrganizationNature getNature() { + return nature; + } + + public void setNature(OrganizationNature nature) { + this.nature = nature; + } + + public byte[] getIcon() { + return icon; + } + + public void setIcon(byte[] icon) { + this.icon = icon; + } + + public String getIconContentType() { + return iconContentType; + } + + public void setIconContentType(String iconContentType) { + this.iconContentType = iconContentType; + } + + public Set getMetadataProperties() { + return metadataProperties; + } + + public void setMetadataProperties(Set metadataProperties) { + this.metadataProperties = metadataProperties; + } + + //@formatter:off + @Override + public String toString() { + return "OrganizationTypeDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", nature='" + getNature() + "'" + + ", icon='" + getIcon() + "'" + + ", metadataProperties=" + getMetadataProperties() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/OutputDataDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/OutputDataDTO.java new file mode 100644 index 0000000..1fe070e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/OutputDataDTO.java @@ -0,0 +1,102 @@ +package com.oguerreiro.resilient.service.dto; + +import java.math.BigDecimal; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.OutputData} entity. + */ +@Schema(description = "Output data Values\nThe values calculated using the Variable.outputFormula, for all variables with Variable.output==TRUE.") +public class OutputDataDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "The evaluated value, calculated by Variable.outputFormula. This will probably represent an emission value.", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal value; + + @Schema(description = "Record version for concurrency control.") + private Integer version; + + private VariableDTO variable; + + private PeriodDTO period; + + private PeriodVersionDTO periodVersion; + + private UnitDTO baseUnit; + + private OrganizationDTO owner; + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public VariableDTO getVariable() { + return variable; + } + + public void setVariable(VariableDTO variable) { + this.variable = variable; + } + + public PeriodDTO getPeriod() { + return period; + } + + public void setPeriod(PeriodDTO period) { + this.period = period; + } + + public PeriodVersionDTO getPeriodVersion() { + return periodVersion; + } + + public void setPeriodVersion(PeriodVersionDTO periodVersion) { + this.periodVersion = periodVersion; + } + + public UnitDTO getBaseUnit() { + return baseUnit; + } + + public void setBaseUnit(UnitDTO baseUnit) { + this.baseUnit = baseUnit; + } + + public OrganizationDTO getOwner() { + return owner; + } + + public void setOwner(OrganizationDTO owner) { + this.owner = owner; + } + + //@formatter:off + @Override + public String toString() { + return "OutputDataDTO{" + + "id=" + getId() + + ", value=" + getValue() + + ", variable=" + getVariable() + + ", period=" + getPeriod() + + ", periodVersion=" + getPeriodVersion() + + ", baseUnit=" + getBaseUnit() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/PasswordChangeDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/PasswordChangeDTO.java new file mode 100644 index 0000000..a125041 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/PasswordChangeDTO.java @@ -0,0 +1,39 @@ +package com.oguerreiro.resilient.service.dto; + +import java.io.Serializable; + +/** + * A DTO representing a password change required data - current and new password. + */ +public class PasswordChangeDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private String currentPassword; + private String newPassword; + + public PasswordChangeDTO() { + // Empty constructor needed for Jackson. + } + + public PasswordChangeDTO(String currentPassword, String newPassword) { + this.currentPassword = currentPassword; + this.newPassword = newPassword; + } + + public String getCurrentPassword() { + return currentPassword; + } + + public void setCurrentPassword(String currentPassword) { + this.currentPassword = currentPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/PeriodDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/PeriodDTO.java new file mode 100644 index 0000000..3a6c55c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/PeriodDTO.java @@ -0,0 +1,127 @@ +package com.oguerreiro.resilient.service.dto; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.Period} entity. + */ +@Schema(description = "Period") +public class PeriodDTO extends ActivityDomainDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Name of the period", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "Description for the period") + private String description; + + @NotNull + @Schema(description = "Period start date", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDate beginDate; + + @NotNull + @Schema(description = "Period end date", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDate endDate; + + @Schema(description = "State of the Period", requiredMode = Schema.RequiredMode.REQUIRED) + private PeriodStatus state; + + @Schema(description = "The creation date (insert). When the data was inserted into the system.", requiredMode = Schema.RequiredMode.REQUIRED) + private Instant creationDate; + + @Schema(description = "The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username.", requiredMode = Schema.RequiredMode.REQUIRED) + private String creationUsername; + + private List periodVersions = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public LocalDate getBeginDate() { + return beginDate; + } + + public void setBeginDate(LocalDate beginDate) { + this.beginDate = beginDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public PeriodStatus getState() { + return state; + } + + public void setState(PeriodStatus state) { + this.state = state; + } + + public Instant getCreationDate() { + return creationDate; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return creationUsername; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public List getPeriodVersions() { + return periodVersions; + } + + public void setPeriodVersions(List periodVersions) { + this.periodVersions = periodVersions; + } + + //@formatter:off + @Override + public String toString() { + return "PeriodDTO{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", beginDate='" + getBeginDate() + "'" + + ", endDate='" + getEndDate() + "'" + + ", state='" + getState() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", periodVersions=" + getPeriodVersions() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/PeriodVersionDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/PeriodVersionDTO.java new file mode 100644 index 0000000..fdc50dc --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/PeriodVersionDTO.java @@ -0,0 +1,114 @@ +package com.oguerreiro.resilient.service.dto; + +import java.time.Instant; + +import com.oguerreiro.resilient.domain.enumeration.PeriodVersionStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.PeriodVersion} entity. + */ +@Schema(description = "Period versions\nAlways have at least one version.") +public class PeriodVersionDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Name of the version", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "Description for the version") + private String description; + + @NotNull + @Schema(description = "Period version (sequencial)", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer periodVersion; + + @NotNull + @Schema(description = "State of the Period", requiredMode = Schema.RequiredMode.REQUIRED) + private PeriodVersionStatus state; + + @NotNull + @Schema(description = "The creation date (insert). When the data was inserted into the system.", requiredMode = Schema.RequiredMode.REQUIRED) + private Instant creationDate; + + @NotNull + @Schema(description = "The actual username logged into the system, that inserted the data. NOT a reference to the user, but the login username.", requiredMode = Schema.RequiredMode.REQUIRED) + private String creationUsername; + + private PeriodDTO period; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getPeriodVersion() { + return periodVersion; + } + + public void setPeriodVersion(Integer periodVersion) { + this.periodVersion = periodVersion; + } + + public PeriodVersionStatus getState() { + return state; + } + + public void setState(PeriodVersionStatus state) { + this.state = state; + } + + public Instant getCreationDate() { + return creationDate; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return creationUsername; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public PeriodDTO getPeriod() { + return period; + } + + public void setPeriod(PeriodDTO period) { + this.period = period; + } + + //@formatter:off + @Override + public String toString() { + return "PeriodVersionDTO{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", periodVersion=" + getPeriodVersion() + + ", state='" + getState() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + ", period=" + getPeriod() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/ResilientLogDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/ResilientLogDTO.java new file mode 100644 index 0000000..1ea1aca --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/ResilientLogDTO.java @@ -0,0 +1,93 @@ +package com.oguerreiro.resilient.service.dto; + +import java.time.Instant; + +import com.oguerreiro.resilient.domain.enumeration.ResilientLogLevel; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.ResilientLog} entity. + */ +public class ResilientLogDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private Long ownerId; + + @NotNull + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private ResilientLogLevel level; + + @NotNull + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private String logMessage; + + @NotNull + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private Instant creationDate; + + @NotNull + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private String creationUsername; + + @Schema(description = "Record version for concurrency control.") + private Integer version; + + public String getLogMessage() { + return logMessage; + } + + public void setLogMessage(String logMessage) { + this.logMessage = logMessage; + } + + public Instant getCreationDate() { + return creationDate; + } + + public void setCreationDate(Instant creationDate) { + this.creationDate = creationDate; + } + + public String getCreationUsername() { + return creationUsername; + } + + public void setCreationUsername(String creationUsername) { + this.creationUsername = creationUsername; + } + + public Long getOwnerId() { + return ownerId; + } + + public void setOwnerId(Long ownerId) { + this.ownerId = ownerId; + } + + public ResilientLogLevel getLevel() { + return level; + } + + public void setLevel(ResilientLogLevel level) { + this.level = level; + } + + //@formatter:off + @Override + public String toString() { + return "InputDataUploadLogDTO{" + + "id=" + getId() + + ", ownerId=" + getOwnerId() + + ", level=" + getLevel() + + ", logMessage='" + getLogMessage() + "'" + + ", creationDate='" + getCreationDate() + "'" + + ", creationUsername='" + getCreationUsername() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/UnitConverterDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/UnitConverterDTO.java new file mode 100644 index 0000000..9cca01b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/UnitConverterDTO.java @@ -0,0 +1,81 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.UnitConverter} entity. + */ +@Schema(description = "Unit Converter") +public class UnitConverterDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + private String name; + + @NotNull + private String description; + + @NotNull + @Schema(description = "The convertion formula to convert the fromUnit into the toUnit.", requiredMode = Schema.RequiredMode.REQUIRED) + private String convertionFormula; + + private UnitDTO fromUnit; + + private UnitDTO toUnit; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getConvertionFormula() { + return convertionFormula; + } + + public void setConvertionFormula(String convertionFormula) { + this.convertionFormula = convertionFormula; + } + + public UnitDTO getFromUnit() { + return fromUnit; + } + + public void setFromUnit(UnitDTO fromUnit) { + this.fromUnit = fromUnit; + } + + public UnitDTO getToUnit() { + return toUnit; + } + + public void setToUnit(UnitDTO toUnit) { + this.toUnit = toUnit; + } + + //@formatter:off + @Override + public String toString() { + return "UnitConverterDTO{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", convertionFormula='" + getConvertionFormula() + "'" + + ", fromUnit=" + getFromUnit() + + ", toUnit=" + getToUnit() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/UnitDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/UnitDTO.java new file mode 100644 index 0000000..5b0e0f6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/UnitDTO.java @@ -0,0 +1,98 @@ +package com.oguerreiro.resilient.service.dto; + +import java.math.BigDecimal; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.Unit} entity. + */ +@Schema(description = "Unit") +public class UnitDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Required code to use in integrations", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Schema(description = "Math symbol that is usually used as sufix for this unit. Eg. 'm3' for square metters.", requiredMode = Schema.RequiredMode.REQUIRED) + private String symbol; + + @NotNull + @Schema(description = "Math name for this unit. Eg. 'Square metters'.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + private String description; + + @NotNull + @Schema(description = "The convertion rate (CR), relative to the base unit.\nThis values awnsers the question: When the base unit is 1 (one), whats the value of this unit measure ?\nEx: How many Ml (miligrams) 1Kg has? 1000\nThe formula to convert between any value unit the scale is:\n*CR(target unit)/CR(source unit)", requiredMode = Schema.RequiredMode.REQUIRED) + private BigDecimal convertionRate; + + private UnitTypeDTO unitType; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public BigDecimal getConvertionRate() { + return convertionRate; + } + + public void setConvertionRate(BigDecimal convertionRate) { + this.convertionRate = convertionRate; + } + + public UnitTypeDTO getUnitType() { + return unitType; + } + + public void setUnitType(UnitTypeDTO unitType) { + this.unitType = unitType; + } + + //@formatter:off + @Override + public String toString() { + return "UnitDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", symbol='" + getSymbol() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", convertionRate=" + getConvertionRate() + + ", unitType=" + getUnitType() + + ", version=" + getVersion() + + "}"; + } + //@formatter:off +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/UnitTypeDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/UnitTypeDTO.java new file mode 100644 index 0000000..17ec90f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/UnitTypeDTO.java @@ -0,0 +1,60 @@ +package com.oguerreiro.resilient.service.dto; + +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.UnitType} entity. + */ +@Schema(description = "UnitType\nType of measure unit types. Convertions can only be done within Unit's of the same UnitType") +public class UnitTypeDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + private String name; + + @NotNull + private String description; + + @NotNull + private UnitValueType valueType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public UnitValueType getValueType() { + return valueType; + } + + public void setValueType(UnitValueType valueType) { + this.valueType = valueType; + } + + //@formatter:off + @Override + public String toString() { + return "UnitTypeDTO{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", valueType='" + getValueType() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:off +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/UserDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/UserDTO.java new file mode 100644 index 0000000..20710b6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/UserDTO.java @@ -0,0 +1,69 @@ +package com.oguerreiro.resilient.service.dto; + +import com.oguerreiro.resilient.domain.User; +import java.io.Serializable; +import java.util.Objects; + +/** + * A DTO representing a user, with only the public attributes. + */ +public class UserDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String login; + + public UserDTO() { + // Empty constructor needed for Jackson. + } + + public UserDTO(User user) { + this.id = user.getId(); + // Customize it here if you need, or not, firstName/lastName/etc + this.login = user.getLogin(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UserDTO userDTO = (UserDTO) o; + if (userDTO.getId() == null || getId() == null) { + return false; + } + + return Objects.equals(getId(), userDTO.getId()) && Objects.equals(getLogin(), userDTO.getLogin()); + } + + // prettier-ignore + @Override + public String toString() { + return "UserDTO{" + + "id='" + id + '\'' + + ", login='" + login + '\'' + + "}"; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/VariableCategoryDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/VariableCategoryDTO.java new file mode 100644 index 0000000..16333c4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/VariableCategoryDTO.java @@ -0,0 +1,94 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.VariableCategory} entity. + */ +@Schema(description = "Variable Category\nSub-division of VariableScope in Categories") +public class VariableCategoryDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Schema(description = "Unique key, user defined.\nBe aware, that this will be used to create the Variable.code\nNOTE: The field type is string, but restricted to only numbers", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Schema(description = "The name or title for this category", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @NotNull + @Schema(description = "A description or usage of this category", requiredMode = Schema.RequiredMode.REQUIRED) + private String description; + + private VariableScopeDTO variableScope; + + private Boolean hiddenForData; + + private Boolean hiddenForFactor; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public VariableScopeDTO getVariableScope() { + return variableScope; + } + + public void setVariableScope(VariableScopeDTO variableScope) { + this.variableScope = variableScope; + } + + public Boolean getHiddenForData() { + return hiddenForData; + } + + public void setHiddenForData(Boolean hiddenForData) { + this.hiddenForData = hiddenForData; + } + + public Boolean getHiddenForFactor() { + return hiddenForFactor; + } + + public void setHiddenForFactor(Boolean hiddenForFactor) { + this.hiddenForFactor = hiddenForFactor; + } + + //@formatter:off + @Override + public String toString() { + return "VariableCategoryDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", variableScope=" + getVariableScope() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/VariableClassDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/VariableClassDTO.java new file mode 100644 index 0000000..f0a89b4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/VariableClassDTO.java @@ -0,0 +1,59 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.VariableClass} entity. + */ +@Schema(description = "Variable Class\nA sub-variable classification. Example: Variable=FUEL CONSUMPTIOM; CLASS=GASOLINE; OR ELECTRICAL; ETC...") +public class VariableClassDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Code of the class. Must be unique within the Variavle", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Schema(description = "Human name of the class. Must be unique within the Variable, because it will be used as key for the Excel integration data.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + private VariableClassTypeDTO variableClassType; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public VariableClassTypeDTO getVariableClassType() { + return variableClassType; + } + + public void setVariableClassType(VariableClassTypeDTO variableClassType) { + this.variableClassType = variableClassType; + } + + //@formatter:off + @Override + public String toString() { + return "VariableClassDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", variableClassType=" + getVariableClassType() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/VariableClassTypeDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/VariableClassTypeDTO.java new file mode 100644 index 0000000..3ce7bb4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/VariableClassTypeDTO.java @@ -0,0 +1,99 @@ +package com.oguerreiro.resilient.service.dto; + +import java.util.HashSet; +import java.util.Set; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.VariableClassType} entity. + */ +@Schema(description = "VariableClassType") +public class VariableClassTypeDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "The internal name for the class type.", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @NotNull + @Size(min = 1) + @Schema(description = "User defined code. Must be unique.", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + private String description; + + @NotNull + @Schema(description = "Label for the class to be used in user interface. Might be overriden by Variable configuration.", requiredMode = Schema.RequiredMode.REQUIRED) + private String label; + + private Set variableClasses = new HashSet<>(); + + @Schema(description = "Record version for concurrency control.") + private Integer version; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Set getVariableClasses() { + return variableClasses; + } + + public void setVariableClasses(Set variableClasses) { + this.variableClasses = variableClasses; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + +//@formatter:off + @Override + public String toString() { + return "VariableClassTypeDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", label='" + getLabel() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/VariableDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/VariableDTO.java new file mode 100644 index 0000000..42ce673 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/VariableDTO.java @@ -0,0 +1,246 @@ +package com.oguerreiro.resilient.service.dto; + +import java.util.HashSet; +import java.util.Set; + +import com.oguerreiro.resilient.domain.enumeration.InputMode; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.Variable} entity. + */ +@Schema(description = "Variable Definition (GEE variable classification)") +public class VariableDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Schema(description = "Unique key, composed by:
\nVariableScope.code + VariableCategory.code + \nNOTA: User will provide the index property, and the software will create the code\nindex and code are NOT changeable", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @Schema(description = "Variable index", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer variableIndex; + + @NotNull + @Schema(description = "The name or title for the variable", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @NotNull + @Schema(description = "The descriptions of the variable. Its meaning. Its usage.", requiredMode = Schema.RequiredMode.REQUIRED) + private String description; + + @NotNull + @Schema(description = "Defines this variable as input. Values will be inserted for this variable", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean input; + + @Schema(description = "Defines the way of input. See the enum for instructions.", requiredMode = Schema.RequiredMode.REQUIRED) + private InputMode inputMode; + + @NotNull + @Schema(description = "Defines this variable as output. This variable will be evaluated for output", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean output; + + private Boolean outputSingle; + + private OrganizationDTO outputOwner; + + @Schema(description = "The formula to calculate the output valut of the variable. Mandatory when output==TRUE") + private String outputFormula; + + @Schema(description = "The scale to use to round the value") + private Integer valueScale; + + @NotNull + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private UnitDTO baseUnit; + + private VariableScopeDTO variableScope; + private VariableCategoryDTO variableCategory; + private VariableClassTypeDTO variableClassType; + + private String variableClassLabel; + private Boolean variableClassMandatory; + private Boolean hiddenForMain; + + private Set variableUnits = new HashSet<>(); + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Integer getVariableIndex() { + return variableIndex; + } + + public void setVariableIndex(Integer variableIndex) { + this.variableIndex = variableIndex; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getInput() { + return input; + } + + public void setInput(Boolean input) { + this.input = input; + } + + public InputMode getInputMode() { + return inputMode; + } + + public void setInputMode(InputMode inputMode) { + this.inputMode = inputMode; + } + + public Boolean getOutput() { + return output; + } + + public void setOutput(Boolean output) { + this.output = output; + } + + public String getOutputFormula() { + return outputFormula; + } + + public void setOutputFormula(String outputFormula) { + this.outputFormula = outputFormula; + } + + public Integer getValueScale() { + return valueScale; + } + + public void setValueScale(Integer valueScale) { + this.valueScale = valueScale; + } + + public UnitDTO getBaseUnit() { + return baseUnit; + } + + public void setBaseUnit(UnitDTO baseUnit) { + this.baseUnit = baseUnit; + } + + public VariableScopeDTO getVariableScope() { + return variableScope; + } + + public void setVariableScope(VariableScopeDTO variableScope) { + this.variableScope = variableScope; + } + + public VariableCategoryDTO getVariableCategory() { + return variableCategory; + } + + public void setVariableCategory(VariableCategoryDTO variableCategory) { + this.variableCategory = variableCategory; + } + + public VariableClassTypeDTO getVariableClassType() { + return variableClassType; + } + + public void setVariableClassType(VariableClassTypeDTO variableClassType) { + this.variableClassType = variableClassType; + } + + public Set getVariableUnits() { + return variableUnits; + } + + public void setVariableUnits(Set variableUnits) { + this.variableUnits = variableUnits; + } + + public String getVariableClassLabel() { + return variableClassLabel; + } + + public void setVariableClassLabel(String variableClassLabel) { + this.variableClassLabel = variableClassLabel; + } + + public Boolean getVariableClassMandatory() { + return variableClassMandatory; + } + + public void setVariableClassMandatory(Boolean variableClassMandatory) { + this.variableClassMandatory = variableClassMandatory; + } + + public Boolean getHiddenForMain() { + return hiddenForMain; + } + + public void setHiddenForMain(Boolean hiddenForMain) { + this.hiddenForMain = hiddenForMain; + } + + public Boolean getOutputSingle() { + return outputSingle; + } + + public void setOutputSingle(Boolean outputSingle) { + this.outputSingle = outputSingle; + } + + public OrganizationDTO getOutputOwner() { + return outputOwner; + } + + public void setOutputOwner(OrganizationDTO outputOwner) { + this.outputOwner = outputOwner; + } + + //@formatter:off + @Override + public String toString() { + return "VariableDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", variableIndex=" + getVariableIndex() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", input='" + getInput() + "'" + + ", inputMode='" + getInputMode() + "'" + + ", output='" + getOutput() + "'" + + ", outputFormula='" + getOutputFormula() + "'" + + ", valueScale=" + getValueScale() + + ", baseUnit=" + getBaseUnit() + + ", variableScope=" + getVariableScope() + + ", variableCategory=" + getVariableCategory() + + ", variableClasstype=" + getVariableClassType() + + ", variableUnits=" + getVariableUnits() + + ", hiddenForMain=" + getHiddenForMain() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/VariableScopeDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/VariableScopeDTO.java new file mode 100644 index 0000000..edb0a5c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/VariableScopeDTO.java @@ -0,0 +1,82 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.VariableScope} entity. + */ +@Schema(description = "Variable Scope\nExample: GEE Scope 1/2/3 or General Info") +public class VariableScopeDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + @NotNull + @Pattern(regexp = "^[0-9]*$") + @Schema(description = "Unique key, user defined.\nBe aware, that this will be used to create the Variable.code\nNOTE: The field type is string, but restricted to only numbers", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotNull + @Schema(description = "The name or title for this scope", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @NotNull + @Schema(description = "A description or usage of this scope", requiredMode = Schema.RequiredMode.REQUIRED) + private String description; + + private Boolean hiddenForData; + private Boolean hiddenForFactor; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getHiddenForData() { + return hiddenForData; + } + + public void setHiddenForData(Boolean hiddenForData) { + this.hiddenForData = hiddenForData; + } + + public Boolean getHiddenForFactor() { + return hiddenForFactor; + } + + public void setHiddenForFactor(Boolean hiddenForFactor) { + this.hiddenForFactor = hiddenForFactor; + } + + //@formatter:off + @Override + public String toString() { + return "VariableScopeDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/VariableUnitsDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/VariableUnitsDTO.java new file mode 100644 index 0000000..299572e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/VariableUnitsDTO.java @@ -0,0 +1,54 @@ +package com.oguerreiro.resilient.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * A DTO for the {@link com.oguerreiro.resilient.domain.VariableUnits} entity. + */ +@Schema(description = "Variable allowed input units. Also defines the converters needed if the") +public class VariableUnitsDTO extends AbstractResilientDTO { + private static final long serialVersionUID = 1L; + + private VariableDTO variable; + + private UnitDTO unit; + + private UnitConverterDTO unitConverter; + + public VariableDTO getVariable() { + return variable; + } + + public void setVariable(VariableDTO variable) { + this.variable = variable; + } + + public UnitDTO getUnit() { + return unit; + } + + public void setUnit(UnitDTO unit) { + this.unit = unit; + } + + public UnitConverterDTO getUnitConverter() { + return unitConverter; + } + + public void setUnitConverter(UnitConverterDTO unitConverter) { + this.unitConverter = unitConverter; + } + + //@formatter:off + @Override + public String toString() { + return "VariableUnitsDTO{" + + "id=" + getId() + + ", variable=" + getVariable() + + ", unit=" + getUnit() + + ", unitConverter=" + getUnitConverter() + + ", version=" + getVersion() + + "}"; + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInfoDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInfoDTO.java new file mode 100644 index 0000000..1574d2b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInfoDTO.java @@ -0,0 +1,19 @@ +package com.oguerreiro.resilient.service.dto.activity; + +import java.io.Serializable; + +public class ActivityInfoDTO implements Serializable { + String activityKey; + + public ActivityInfoDTO(String activityKey) { + this.activityKey = activityKey; + } + + public String getActivityKey() { + return activityKey; + } + + public void setActivityKey(String activityKey) { + this.activityKey = activityKey; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInvokeDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInvokeDTO.java new file mode 100644 index 0000000..d3f7521 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInvokeDTO.java @@ -0,0 +1,42 @@ +package com.oguerreiro.resilient.service.dto.activity; + +import java.io.Serializable; + +/** + * Used from Frontend to invoke BackEnd. + * Defines a Activity execution call. This simplifies the endpoint, and the arguments are sent in a POST request. + * Also simplifies future evolution of the Activity call, by simply adding more properties to the ActivityInvokeDTO. + * + * This DTO doesn't need a mapper + */ +public class ActivityInvokeDTO implements Serializable { + String activityKey; + String activityDomainClass; + Long activityDomainId; + + public String getActivityKey() { + return activityKey; + } + + public void setActivityKey(String activityKey) { + this.activityKey = activityKey; + } + + public String getActivityDomainClass() { + return activityDomainClass; + } + + public void setActivityDomainClass(String activityDomainClass) { + this.activityDomainClass = activityDomainClass; + } + + public Long getActivityDomainId() { + return activityDomainId; + } + + public void setActivityDomainId(Long activityDomainId) { + this.activityDomainId = activityDomainId; + } + + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInvokeResponseDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInvokeResponseDTO.java new file mode 100644 index 0000000..40991d4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityInvokeResponseDTO.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.service.dto.activity; + +import java.io.Serializable; + +/** + * Used from BackEnd to invoke Frontend. + * This DTO is intended for the response to a Activity Invoke request. + * + * This DTO doesn't need a mapper + */ +public class ActivityInvokeResponseDTO implements Serializable { + String activityUUID; + + public String getActivityUUID() { + return activityUUID; + } + + public void setActivityUUID(String activityUUID) { + this.activityUUID = activityUUID; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityProgressDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityProgressDTO.java new file mode 100644 index 0000000..6afd084 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/activity/ActivityProgressDTO.java @@ -0,0 +1,38 @@ +package com.oguerreiro.resilient.service.dto.activity; + +import java.io.Serializable; + +import jakarta.validation.constraints.NotNull; + +public class ActivityProgressDTO implements Serializable { + @NotNull + private Integer progressPercentage; + @NotNull + private String progressTask; + + public Integer getProgressPercentage() { + return progressPercentage; + } + + public void setProgressPercentage(Integer progressPercentage) { + this.progressPercentage = progressPercentage; + } + + public String getProgressTask() { + return progressTask; + } + + public void setProgressTask(String progressTask) { + this.progressTask = progressTask; + } + + // prettier-ignore + @Override + public String toString() { + return "ActivityProgressDTO{" + + "progressPercentage=" + getProgressPercentage() + + "progressTask=" + getProgressTask() + + "}"; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/package-info.java b/src/main/java/com/oguerreiro/resilient/service/dto/package-info.java new file mode 100644 index 0000000..8a54c13 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * Data transfer objects for rest mapping. + */ +package com.oguerreiro.resilient.service.dto; diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityGroupDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityGroupDTO.java new file mode 100644 index 0000000..a01a865 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityGroupDTO.java @@ -0,0 +1,80 @@ +package com.oguerreiro.resilient.service.dto.security; + +import java.util.ArrayList; +import java.util.List; + +import com.oguerreiro.resilient.security.resillient.SecurityGroup; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.security.SecurityGroup} entity. + */ +@Schema(description = "SecurityGroup") +public class SecurityGroupDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + @NotNull + @Schema(description = "Required and unique code for the group", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + private String name; + + private List securityGroupPermissions = new ArrayList<>(); + + // Needed for Jackson + public SecurityGroupDTO() { + } + + public SecurityGroupDTO(SecurityGroup securityGroup) { + if (securityGroup == null) + return; + + this.setId(securityGroup.getId()); + this.setVersion(securityGroup.getVersion()); + this.code = securityGroup.getCode(); + this.name = securityGroup.getName(); + + this.securityGroupPermissions = securityGroup.getSecurityGroupPermissions().stream().map( + SecurityGroupPermissionDTO::new).toList(); + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getSecurityGroupPermissions() { + return securityGroupPermissions; + } + + public void setSecurityGroupPermissions(List securityGroupPermissions) { + this.securityGroupPermissions = securityGroupPermissions; + } + + //@formatter:off + @Override + public String toString() { + return "SecurityGroupDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityGroupPermissionDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityGroupPermissionDTO.java new file mode 100644 index 0000000..a28b64c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityGroupPermissionDTO.java @@ -0,0 +1,103 @@ +package com.oguerreiro.resilient.service.dto.security; + +import com.oguerreiro.resilient.security.resillient.SecurityGroupPermission; +import com.oguerreiro.resilient.security.resillient.SecurityPermission; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * A DTO for the {@link com.oguerreiro.resilient.security.SecurityGroupPermission} entity. + */ +@Schema(description = "SecurityGroupPermission") +public class SecurityGroupPermissionDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + private SecurityGroupDTO securityGroup; + private SecurityResourceDTO securityResource; + private SecurityPermission createPermission; + private SecurityPermission readPermission; + private SecurityPermission updatePermission; + private SecurityPermission deletePermission; + + // Needed for Jackson + public SecurityGroupPermissionDTO() { + } + + public SecurityGroupPermissionDTO(SecurityGroupPermission securityGroupPermission) { + // Don't know why I need this. In some cases the collection.stream() invoques the mapper with a NULL ??!! + if (securityGroupPermission == null) { + return; + } + this.securityResource = new SecurityResourceDTO(); + this.securityResource.setCode(securityGroupPermission.getSecurityResource().getCode()); + + this.createPermission = securityGroupPermission.getCreatePermission(); + this.readPermission = securityGroupPermission.getReadPermission(); + this.updatePermission = securityGroupPermission.getUpdatePermission(); + this.deletePermission = securityGroupPermission.getDeletePermission(); + } + + public SecurityGroupDTO getSecurityGroup() { + return securityGroup; + } + + public void setSecurityGroup(SecurityGroupDTO securityGroup) { + this.securityGroup = securityGroup; + } + + public SecurityResourceDTO getSecurityResource() { + return securityResource; + } + + public void setSecurityResource(SecurityResourceDTO securityResource) { + this.securityResource = securityResource; + } + + public SecurityPermission getCreatePermission() { + return createPermission; + } + + public void setCreatePermission(SecurityPermission createPermission) { + this.createPermission = createPermission; + } + + public SecurityPermission getReadPermission() { + return readPermission; + } + + public void setReadPermission(SecurityPermission readPermission) { + this.readPermission = readPermission; + } + + public SecurityPermission getUpdatePermission() { + return updatePermission; + } + + public void setUpdatePermission(SecurityPermission updatePermission) { + this.updatePermission = updatePermission; + } + + public SecurityPermission getDeletePermission() { + return deletePermission; + } + + public void setDeletePermission(SecurityPermission deletePermission) { + this.deletePermission = deletePermission; + } + + //@formatter:off + @Override + public String toString() { + return "SecurityGroupDTO{" + + "id=" + getId() + + ", createPermission='" + getCreatePermission() + "'" + + ", readPermission='" + getReadPermission() + "'" + + ", updatePermission='" + getUpdatePermission() + "'" + + ", deletePermission='" + getDeletePermission() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityResourceDTO.java b/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityResourceDTO.java new file mode 100644 index 0000000..f0c0fb7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/dto/security/SecurityResourceDTO.java @@ -0,0 +1,109 @@ +package com.oguerreiro.resilient.service.dto.security; + +import com.oguerreiro.resilient.security.resillient.SecurityPermission; +import com.oguerreiro.resilient.security.resillient.SecurityResourceType; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** + * A DTO for the {@link com.oguerreiro.resilient.security.SecurityResource} entity. + */ +@Schema(description = "SecurityResource") +public class SecurityResourceDTO extends AbstractResilientDTO { + + private static final long serialVersionUID = 1L; + + @NotNull + private String code; + + private String name; + + @NotNull + private SecurityResourceType type; + + @NotNull + private SecurityPermission defaultCreatePermission; + + @NotNull + private SecurityPermission defaultReadPermission; + + @NotNull + private SecurityPermission defaultUpdatePermission; + + @NotNull + private SecurityPermission defaultDeletePermission; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public SecurityResourceType getType() { + return type; + } + + public void setType(SecurityResourceType type) { + this.type = type; + } + + public SecurityPermission getDefaultCreatePermission() { + return defaultCreatePermission; + } + + public void setDefaultCreatePermission(SecurityPermission defaultCreatePermission) { + this.defaultCreatePermission = defaultCreatePermission; + } + + public SecurityPermission getDefaultReadPermission() { + return defaultReadPermission; + } + + public void setDefaultReadPermission(SecurityPermission defaultReadPermission) { + this.defaultReadPermission = defaultReadPermission; + } + + public SecurityPermission getDefaultUpdatePermission() { + return defaultUpdatePermission; + } + + public void setDefaultUpdatePermission(SecurityPermission defaultUpdatePermission) { + this.defaultUpdatePermission = defaultUpdatePermission; + } + + public SecurityPermission getDefaultDeletePermission() { + return defaultDeletePermission; + } + + public void setDefaultDeletePermission(SecurityPermission defaultDeletePermission) { + this.defaultDeletePermission = defaultDeletePermission; + } + + //@formatter:off + @Override + public String toString() { + return "SecurityResourceDTO{" + + "id=" + getId() + + ", code='" + getCode() + "'" + + ", name='" + getName() + "'" + + ", defaultCreatePermission='" + getDefaultCreatePermission() + "'" + + ", defaultReadPermission='" + getDefaultReadPermission() + "'" + + ", defaultUpdatePermission='" + getDefaultUpdatePermission() + "'" + + ", defaultDeletePermission='" + getDefaultDeletePermission() + "'" + + ", version=" + getVersion() + + "}"; + } +//@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/service/interceptors/AfterSaveServiceInterceptor.java b/src/main/java/com/oguerreiro/resilient/service/interceptors/AfterSaveServiceInterceptor.java new file mode 100644 index 0000000..32c437e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/interceptors/AfterSaveServiceInterceptor.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.service.interceptors; + +import java.io.Serializable; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +/** + * Interceptor called AFTER the domainRepository.save is called. Any exception raised, will fail the transaction. + * CHANGES made to domain, will be lost, since the save already occurred. But changes will be used by the mapper, that + * will be invoked next.
+ * Implement this in a Server impl class + * + * @param + * @param + * @param + */ +public interface AfterSaveServiceInterceptor, T extends AbstractResilientEntity, ID extends Serializable> { + T afterSave(T domain); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/interceptors/AfterUpdateServiceInterceptor.java b/src/main/java/com/oguerreiro/resilient/service/interceptors/AfterUpdateServiceInterceptor.java new file mode 100644 index 0000000..b8b22cb --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/interceptors/AfterUpdateServiceInterceptor.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient.service.interceptors; + +import java.io.Serializable; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +/** + * Interceptor called AFTER the domainRepository.save is called. Any exception raised, will fail the transaction. + * CHANGES made to domain, will be lost, since the save already occurred. But changes will be used by the mapper, that + * will be invoked next.
+ * Implement this in a Server impl class + * + * @param + * @param + * @param + */ +public interface AfterUpdateServiceInterceptor, T extends AbstractResilientEntity, ID extends Serializable> { + T afterUpdate(T domain); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeDeleteServiceInterceptor.java b/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeDeleteServiceInterceptor.java new file mode 100644 index 0000000..8d3050a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeDeleteServiceInterceptor.java @@ -0,0 +1,17 @@ +package com.oguerreiro.resilient.service.interceptors; + +import java.io.Serializable; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +/** + * Interceptor called BEFORE the domainRepository.save is called. + * + * @param + * @param + * @param + */ +public interface BeforeDeleteServiceInterceptor, T extends AbstractResilientEntity, ID extends Serializable> { + void beforeDelete(ID id); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeSaveServiceInterceptor.java b/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeSaveServiceInterceptor.java new file mode 100644 index 0000000..923f811 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeSaveServiceInterceptor.java @@ -0,0 +1,17 @@ +package com.oguerreiro.resilient.service.interceptors; + +import java.io.Serializable; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +/** + * Interceptor called BEFORE the domainRepository.save is called. + * + * @param + * @param + * @param + */ +public interface BeforeSaveServiceInterceptor, T extends AbstractResilientEntity, ID extends Serializable> { + T beforeSave(T domain); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeUpdateServiceInterceptor.java b/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeUpdateServiceInterceptor.java new file mode 100644 index 0000000..a9cd41e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/interceptors/BeforeUpdateServiceInterceptor.java @@ -0,0 +1,17 @@ +package com.oguerreiro.resilient.service.interceptors; + +import java.io.Serializable; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; + +/** + * Interceptor called BEFORE the domainRepository.save is called. + * + * @param + * @param + * @param + */ +public interface BeforeUpdateServiceInterceptor, T extends AbstractResilientEntity, ID extends Serializable> { + T beforeUpdate(T domain); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/ActivityProgressMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/ActivityProgressMapper.java new file mode 100644 index 0000000..477cb01 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/ActivityProgressMapper.java @@ -0,0 +1,13 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.Mapper; + +import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor; +import com.oguerreiro.resilient.service.dto.activity.ActivityProgressDTO; + +/** + * Mapper for the entity {@link ActivityProgressDescriptor} and its DTO {@link ActivityProgressDescriptorDTO}. + */ +@Mapper(componentModel = "spring") +public interface ActivityProgressMapper extends ResilientEntityMapper { +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/ContentPageMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/ContentPageMapper.java new file mode 100644 index 0000000..3e410c2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/ContentPageMapper.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.Mapper; + +import com.oguerreiro.resilient.domain.ContentPage; +import com.oguerreiro.resilient.service.dto.ContentPageDTO; + +/** + * Mapper for the entity {@link ContentPage} and its DTO {@link ContentPageDTO}. + */ +@Mapper(componentModel = "spring") +public interface ContentPageMapper extends ResilientEntityMapper { + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardComponentDetailValueMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardComponentDetailValueMapper.java new file mode 100644 index 0000000..e614505 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardComponentDetailValueMapper.java @@ -0,0 +1,78 @@ +package com.oguerreiro.resilient.service.mapper; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.DashboardComponent; +import com.oguerreiro.resilient.domain.DashboardComponentDetail; +import com.oguerreiro.resilient.domain.DashboardComponentDetailValue; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.service.dto.DashboardComponentDTO; +import com.oguerreiro.resilient.service.dto.DashboardComponentDetailDTO; +import com.oguerreiro.resilient.service.dto.DashboardComponentDetailValueDTO; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; + +/** + * Mapper for the entity {@link DashboardComponentDetailValue} and its DTO {@link DashboardComponentDetailValueDTO}. + */ +@Mapper(componentModel = "spring") +public interface DashboardComponentDetailValueMapper { + // extends ResilientEntityMapper { + @Mapping(target = "dashboardComponentDetail", source = "dashboardComponentDetail", qualifiedByName = "dashboardComponentDetailPropertyName") + @Mapping(target = "targetDashboardComponent", source = "targetDashboardComponent", qualifiedByName = "targetDashboardComponentPropertyName") + @Mapping(target = "organization", source = "owner", qualifiedByName = "organizationPropertyName") + DashboardComponentDetailValueDTO toDto(DashboardComponentDetailValue dashboardComponentDetailValue); + + @Named("dashboardComponentDetailPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "color", source = "color") + @Mapping(target = "help", source = "help") + @Mapping(target = "variable", source = "variable", qualifiedByName = "variablePropertyName") + DashboardComponentDetailDTO toDtoDashboardComponentDetailPropertyName( + DashboardComponentDetail dashboardComponentDetail); + + @Named("targetDashboardComponentPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + DashboardComponentDTO toDtoTargetDashboardComponentPropertyName(DashboardComponent targetDashboardComponent); + + @Named("variablePropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + VariableDTO toDtoVariablePropertyName(Variable variable); + + @Named("organizationPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "code", source = "code") + OrganizationDTO toDtoOrganizationPropertyName(Organization organization); + + default Map> mapToDto( + Map> entityMap) { + Map> newMap = new LinkedHashMap>(); + + for (Entry> entry : entityMap.entrySet()) { + List details = new ArrayList(); + + for (DashboardComponentDetailValue dashboardComponentDetailValue : entry.getValue()) { + details.add(toDto(dashboardComponentDetailValue)); + } + + newMap.put(entry.getKey(), details); + } + + return newMap; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardComponentMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardComponentMapper.java new file mode 100644 index 0000000..a206340 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardComponentMapper.java @@ -0,0 +1,90 @@ +package com.oguerreiro.resilient.service.mapper; + +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.DashboardComponent; +import com.oguerreiro.resilient.domain.DashboardComponentDetail; +import com.oguerreiro.resilient.domain.DashboardComponentOrganization; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.service.dto.DashboardComponentDTO; +import com.oguerreiro.resilient.service.dto.DashboardComponentDetailDTO; +import com.oguerreiro.resilient.service.dto.DashboardComponentOrganizationDTO; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; + +/** + * Mapper for the entity {@link DashboardComponent} and its DTO {@link DashboardComponentDTO}. + */ +@Mapper(componentModel = "spring") +public interface DashboardComponentMapper extends ResilientEntityMapper { + @Mapping(target = "dashboardComponentDetails", source = "dashboardComponentDetails", qualifiedByName = "dashboardComponentDetailNameList") + @Mapping(target = "dashboardComponentOrganizations", source = "dashboardComponentOrganizations", qualifiedByName = "dashboardComponentOrganizationNameList") + DashboardComponentDTO toDto(DashboardComponent dashboardComponent); + + @Mapping(target = "removeDashboardComponentDetail", ignore = true) + DashboardComponent toEntity(DashboardComponentDTO dashboardComponentDTO); + + @Named("variableName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "code", source = "code") // Needed to show in the select list immediately, otherwise it waits for full page rendering + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableDTO toDtoVariableName(Variable variable); + + @Named("targetDashboardComponentName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + DashboardComponentDTO toDtoTargetDashboardComponentName(DashboardComponent targetDashboardComponent); + + @Named("dashboardComponentDetailPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "color", source = "color") + @Mapping(target = "help", source = "help") + @Mapping(target = "indexOrder", source = "indexOrder") + @Mapping(target = "variable", source = "variable", qualifiedByName = "variableName") + @Mapping(target = "targetDashboardComponent", source = "targetDashboardComponent", qualifiedByName = "targetDashboardComponentName") + @Mapping(target = "version", source = "version") + DashboardComponentDetailDTO toDtoDashboardComponentDetailPropertyName( + DashboardComponentDetail dashboardComponentDetail); + + @Named("dashboardComponentDetailNameList") + default List toDtoDashboardComponentDetailPropertyNameSet( + List dashboardComponentDetail) { + return dashboardComponentDetail.stream().map(this::toDtoDashboardComponentDetailPropertyName).collect( + Collectors.toList()); + } + + @Named("organizationName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "code", source = "code") + @Mapping(target = "version", source = "version") + OrganizationDTO toDtoOrganizationName(Organization organization); + + @Named("dashboardComponentOrganizationPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "organization", source = "organization", qualifiedByName = "organizationName") + @Mapping(target = "version", source = "version") + DashboardComponentOrganizationDTO toDtoDashboardComponentOrganizationPropertyName( + DashboardComponentOrganization dashboardComponentOrganization); + + @Named("dashboardComponentOrganizationNameList") + default List toDtoDashboardComponentOrganizationPropertyNameSet( + List dashboardComponentOrganization) { + return dashboardComponentOrganization.stream().map(this::toDtoDashboardComponentOrganizationPropertyName).collect( + Collectors.toList()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardMapper.java new file mode 100644 index 0000000..3f4b289 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/DashboardMapper.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.Mapper; + +import com.oguerreiro.resilient.domain.Dashboard; +import com.oguerreiro.resilient.service.dto.DashboardDTO; + +/** + * Mapper for the entity {@link Dashboard} and its DTO {@link DashboardDTO}. + */ +@Mapper(componentModel = "spring") +public interface DashboardMapper extends ResilientEntityMapper { + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/DocumentMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/DocumentMapper.java new file mode 100644 index 0000000..379df79 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/DocumentMapper.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.Mapper; + +import com.oguerreiro.resilient.domain.Document; +import com.oguerreiro.resilient.service.dto.DocumentDTO; + +/** + * Mapper for the entity {@link Document} and its DTO + * {@link DocumentDTO}. + */ +@Mapper(componentModel = "spring") +public interface DocumentMapper extends ResilientEntityMapper { +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/EmissionFactorMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/EmissionFactorMapper.java new file mode 100644 index 0000000..1dd890a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/EmissionFactorMapper.java @@ -0,0 +1,37 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.EmissionFactor; +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.service.dto.EmissionFactorDTO; +import com.oguerreiro.resilient.service.dto.VariableCategoryDTO; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; + +/** + * Mapper for the entity {@link EmissionFactor} and its DTO {@link EmissionFactorDTO}. + */ +@Mapper(componentModel = "spring") +public interface EmissionFactorMapper extends ResilientEntityMapper { + @Mapping(target = "variableScope", source = "variableScope", qualifiedByName = "variableScopeName") + @Mapping(target = "variableCategory", source = "variableCategory", qualifiedByName = "variableCategoryName") + EmissionFactorDTO toDto(EmissionFactor s); + + @Named("variableScopeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableScopeDTO toDtoVariableScopeName(VariableScope variableScope); + + @Named("variableCategoryName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableCategoryDTO toDtoVariableCategoryName(VariableCategory variableCategory); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataMapper.java new file mode 100644 index 0000000..25600a6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataMapper.java @@ -0,0 +1,97 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.service.dto.InputDataDTO; +import com.oguerreiro.resilient.service.dto.InputDataUploadDTO; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.dto.PeriodDTO; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.VariableCategoryDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; + +/** + * Mapper for the entity {@link InputData} and its DTO {@link InputDataDTO}. + */ +@Mapper(componentModel = "spring") +public interface InputDataMapper extends ResilientEntityMapper { + @Mapping(target = "variable", source = "variable", qualifiedByName = "variableName") + @Mapping(target = "period", source = "period", qualifiedByName = "periodName") + @Mapping(target = "sourceUnit", source = "sourceUnit", qualifiedByName = "unitName") + @Mapping(target = "unit", source = "unit", qualifiedByName = "unitName") + @Mapping(target = "owner", source = "owner", qualifiedByName = "organizationName") + @Mapping(target = "sourceInputData", source = "sourceInputData", qualifiedByName = "inputDataId") + @Mapping(target = "sourceInputDataUpload", source = "sourceInputDataUpload", qualifiedByName = "inputDataUploadId") + InputDataDTO toDto(InputData s); + + @Named("inputDataId") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "version", source = "version") + InputDataDTO toDtoInputDataId(InputData inputData); + + @Named("variableScopeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableScopeDTO toDtoVariableScopeName(VariableScope variableScope); + + @Named("variableCategoryName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableCategoryDTO toDtoVariableCategoryName(VariableCategory variableCategory); + + @Named("variableName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "code", source = "code") + @Mapping(target = "variableScope", source = "variableScope", qualifiedByName = "variableScopeName") + @Mapping(target = "variableCategory", source = "variableCategory", qualifiedByName = "variableCategoryName") + @Mapping(target = "baseUnit", source = "baseUnit") + @Mapping(target = "version", source = "version") + VariableDTO toDtoVariableName(Variable variable); + + @Named("periodName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + PeriodDTO toDtoPeriodName(Period period); + + @Named("unitName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "symbol", source = "symbol") + @Mapping(target = "version", source = "version") + UnitDTO toDtoUnitName(Unit unit); + + @Named("organizationName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + OrganizationDTO toDtoOrganizationName(Organization organization); + + @Named("inputDataUploadId") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "version", source = "version") + InputDataUploadDTO toDtoInputDataUploadId(InputDataUpload inputDataUpload); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataUploadLogMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataUploadLogMapper.java new file mode 100644 index 0000000..2da4073 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataUploadLogMapper.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.InputDataUploadLog; +import com.oguerreiro.resilient.service.dto.InputDataUploadDTO; +import com.oguerreiro.resilient.service.dto.InputDataUploadLogDTO; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link InputDataUploadLog} and its DTO {@link InputDataUploadLogDTO}. + */ +@Mapper(componentModel = "spring") +public interface InputDataUploadLogMapper extends ResilientEntityMapper { + @Mapping(target = "inputDataUpload", source = "inputDataUpload", qualifiedByName = "inputDataUploadTitle") + InputDataUploadLogDTO toDto(InputDataUploadLog s); + + @Named("inputDataUploadTitle") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "title", source = "title") + InputDataUploadDTO toDtoInputDataUploadTitle(InputDataUpload inputDataUpload); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataUploadMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataUploadMapper.java new file mode 100644 index 0000000..c30c251 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/InputDataUploadMapper.java @@ -0,0 +1,36 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.service.dto.InputDataUploadDTO; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.dto.PeriodDTO; + +/** + * Mapper for the entity {@link InputDataUpload} and its DTO + * {@link InputDataUploadDTO}. + */ +@Mapper(componentModel = "spring") +public interface InputDataUploadMapper extends ResilientEntityMapper { + @Mapping(target = "period", source = "period", qualifiedByName = "periodName") + @Mapping(target = "owner", source = "owner", qualifiedByName = "organizationName") + InputDataUploadDTO toDto(InputDataUpload s); + + @Named("periodName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + PeriodDTO toDtoPeriodName(Period period); + + @Named("organizationName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + OrganizationDTO toDtoOrganizationName(Organization organization); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/InventoryDataMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/InventoryDataMapper.java new file mode 100644 index 0000000..2e003da --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/InventoryDataMapper.java @@ -0,0 +1,43 @@ +package com.oguerreiro.resilient.service.mapper; + +import java.util.List; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.DashboardComponentDetailValue; +import com.oguerreiro.resilient.domain.InventoryData; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.service.dto.DashboardComponentDetailValueDTO; +import com.oguerreiro.resilient.service.dto.InventoryDataDTO; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; + +/** + * Mapper for the entity {@link DashboardComponentDetailValue} and its DTO {@link DashboardComponentDetailValueDTO}. + */ +@Mapper(componentModel = "spring") +public interface InventoryDataMapper { + // extends ResilientEntityMapper { + @Mapping(target = "variable", source = "variable", qualifiedByName = "variablePropertyName") + @Mapping(target = "unit", source = "unit", qualifiedByName = "unitPropertyName") + InventoryDataDTO toDto(InventoryData inventoryData); + + @Named("unitPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "symbol", source = "symbol") + UnitDTO toDtoUnitPropertyName(Unit unit); + + @Named("variablePropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + VariableDTO toDtoVariablePropertyName(Variable variable); + + List toDto(List inventoryDataList); + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/MetadataPropertyMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/MetadataPropertyMapper.java new file mode 100644 index 0000000..4d3685e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/MetadataPropertyMapper.java @@ -0,0 +1,33 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.MetadataProperty; +import com.oguerreiro.resilient.domain.OrganizationType; +import com.oguerreiro.resilient.service.dto.MetadataPropertyDTO; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import java.util.Set; +import java.util.stream.Collectors; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link MetadataProperty} and its DTO {@link MetadataPropertyDTO}. + */ +@Mapper(componentModel = "spring") +public interface MetadataPropertyMapper extends ResilientEntityMapper { + @Mapping(target = "organizationTypes", source = "organizationTypes", qualifiedByName = "organizationTypeNameSet") + MetadataPropertyDTO toDto(MetadataProperty s); + + @Mapping(target = "organizationTypes", ignore = true) + @Mapping(target = "removeOrganizationType", ignore = true) + MetadataProperty toEntity(MetadataPropertyDTO metadataPropertyDTO); + + @Named("organizationTypeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + OrganizationTypeDTO toDtoOrganizationTypeName(OrganizationType organizationType); + + @Named("organizationTypeNameSet") + default Set toDtoOrganizationTypeNameSet(Set organizationType) { + return organizationType.stream().map(this::toDtoOrganizationTypeName).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/MetadataValueMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/MetadataValueMapper.java new file mode 100644 index 0000000..8066195 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/MetadataValueMapper.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.MetadataValue; +import com.oguerreiro.resilient.service.dto.MetadataValueDTO; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link MetadataValue} and its DTO {@link MetadataValueDTO}. + */ +@Mapper(componentModel = "spring") +public interface MetadataValueMapper extends ResilientEntityMapper {} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/OrganizationMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/OrganizationMapper.java new file mode 100644 index 0000000..491a818 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/OrganizationMapper.java @@ -0,0 +1,45 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.OrganizationType; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import com.oguerreiro.resilient.service.dto.UserDTO; + +/** + * Mapper for the entity {@link Organization} and its DTO {@link OrganizationDTO}. + */ +@Mapper(componentModel = "spring") +public interface OrganizationMapper extends ResilientEntityMapper { + @Mapping(target = "user", source = "user", qualifiedByName = "userLogin") + @Mapping(target = "parent", source = "parent", qualifiedByName = "organizationName") + @Mapping(target = "organizationType", source = "organizationType", qualifiedByName = "organizationTypeName") + OrganizationDTO toDto(Organization s); + + @Named("userLogin") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "login", source = "login") + UserDTO toDtoUserLogin(User user); + + @Named("organizationName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + OrganizationDTO toDtoOrganizationName(Organization organization); + + @Named("organizationTypeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "nature", source = "nature") + @Mapping(target = "version", source = "version") + OrganizationTypeDTO toDtoOrganizationTypeName(OrganizationType organizationType); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/OrganizationTypeMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/OrganizationTypeMapper.java new file mode 100644 index 0000000..7e128b0 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/OrganizationTypeMapper.java @@ -0,0 +1,33 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.MetadataProperty; +import com.oguerreiro.resilient.domain.OrganizationType; +import com.oguerreiro.resilient.service.dto.MetadataPropertyDTO; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import java.util.Set; +import java.util.stream.Collectors; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link OrganizationType} and its DTO {@link OrganizationTypeDTO}. + */ +@Mapper(componentModel = "spring") +public interface OrganizationTypeMapper extends ResilientEntityMapper { + @Mapping(target = "metadataProperties", source = "metadataProperties", qualifiedByName = "metadataPropertyNameSet") + OrganizationTypeDTO toDto(OrganizationType s); + + @Mapping(target = "removeMetadataProperties", ignore = true) + OrganizationType toEntity(OrganizationTypeDTO organizationTypeDTO); + + @Named("metadataPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + MetadataPropertyDTO toDtoMetadataPropertyName(MetadataProperty metadataProperty); + + @Named("metadataPropertyNameSet") + default Set toDtoMetadataPropertyNameSet(Set metadataProperty) { + return metadataProperty.stream().map(this::toDtoMetadataPropertyName).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/OutputDataMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/OutputDataMapper.java new file mode 100644 index 0000000..a7184c5 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/OutputDataMapper.java @@ -0,0 +1,65 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.dto.OutputDataDTO; +import com.oguerreiro.resilient.service.dto.PeriodDTO; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; + +/** + * Mapper for the entity {@link OutputData} and its DTO {@link OutputDataDTO}. + */ +@Mapper(componentModel = "spring") +public interface OutputDataMapper extends ResilientEntityMapper { + @Mapping(target = "variable", source = "variable", qualifiedByName = "variableName") + @Mapping(target = "period", source = "period", qualifiedByName = "periodName") + @Mapping(target = "periodVersion", source = "periodVersion", qualifiedByName = "periodVersionName") + @Mapping(target = "baseUnit", source = "baseUnit", qualifiedByName = "unitName") + @Mapping(target = "owner", source = "owner", qualifiedByName = "organizationName") + OutputDataDTO toDto(OutputData s); + + @Named("variableName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "code", source = "code") + VariableDTO toDtoVariableName(Variable variable); + + @Named("periodName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + PeriodDTO toDtoPeriodName(Period period); + + @Named("periodVersionName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + PeriodVersionDTO toDtoPeriodVersionName(PeriodVersion periodVersion); + + @Named("unitName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "symbol", source = "symbol") + UnitDTO toDtoUnitName(Unit unit); + + @Named("organizationName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + OrganizationDTO toDtoOrganizationName(Organization organization); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/PeriodMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/PeriodMapper.java new file mode 100644 index 0000000..0336037 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/PeriodMapper.java @@ -0,0 +1,75 @@ +package com.oguerreiro.resilient.service.mapper; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.service.dto.PeriodDTO; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; + +/** + * Mapper for the entity {@link Period} and its DTO {@link PeriodDTO}. + */ +@Mapper(componentModel = "spring") +public interface PeriodMapper extends ResilientEntityMapper { + @Mapping(target = "periodVersions", source = "periodVersions", qualifiedByName = "periodVersionNameList") + PeriodDTO toDto(Period s); + + @Mapping(target = "removePeriodVersion", ignore = true) + Period toEntity(PeriodDTO periodDTO); + + @Named("periodVersionPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "description", source = "description") + @Mapping(target = "periodVersion", source = "periodVersion") + @Mapping(target = "state", source = "state") + @Mapping(target = "version", source = "version") + PeriodVersionDTO toDtoPeriodVersionPropertyName(PeriodVersion periodVersion); + + @Named("periodVersionNameList") + default List toDtoPeriodVersionPropertyNameSet(List periodVersion) { + return periodVersion.stream().map(this::toDtoPeriodVersionPropertyName).collect(Collectors.toList()); + } + + /* ********************************************************************* */ + /* MAPPER for update. Restricts to client side updatable properties only */ + /* ********************************************************************* */ + + /** + * {@link PeriodDTO} to {@link Period} updating an existing instance of {@link Period} using concurrency check.
+ * ATTENTION: This is just half of the solution. The nested collection (PeriodVersion's) is to complex to be done by + * the mapper. + * The solution is in the PeriodService.update() + * + * @param periodDTO + * @param period + */ + @Mapping(target = "removePeriodVersion", ignore = true) + @Mapping(target = "state", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUsername", ignore = true) + @Mapping(target = "periodVersions", ignore = true) //Will be managed manually in PeriodService.updateSafe() + void updatePeriodFromDto(PeriodDTO periodDTO, @MappingTarget Period period); + + @Mapping(target = "state", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUsername", ignore = true) + PeriodVersion toPeriodVersion(PeriodVersionDTO dto); + + @Named("periodVersionMap") + default List toEntityPeriodVersionsSet(List dtos) { + if (dtos == null) + return Collections.emptyList(); + return dtos.stream().map(this::toPeriodVersion).collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/PeriodVersionMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/PeriodVersionMapper.java new file mode 100644 index 0000000..bd5e5e1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/PeriodVersionMapper.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.service.dto.PeriodDTO; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link PeriodVersion} and its DTO {@link PeriodVersionDTO}. + */ +@Mapper(componentModel = "spring") +public interface PeriodVersionMapper extends ResilientEntityMapper { + @Mapping(target = "period", source = "period", qualifiedByName = "periodName") + PeriodVersionDTO toDto(PeriodVersion s); + + @Named("periodName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + PeriodDTO toDtoPeriodName(Period period); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/ResilientEntityMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/ResilientEntityMapper.java new file mode 100644 index 0000000..f5ad78e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/ResilientEntityMapper.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.service.mapper; + +import java.util.List; + +import org.mapstruct.BeanMapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Named; +import org.mapstruct.NullValuePropertyMappingStrategy; + +/** + * Contract for a generic dto to entity mapper. + * + * @param - Entity domain DTO class type parameter. + * @param - Entity domain class type parameter. + */ + +public interface ResilientEntityMapper { + T toEntity(DTO dto); + + DTO toDto(T entity); + + List toEntity(List dtoList); + + List toDto(List entityList); + + @Named("partialUpdate") + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void partialUpdate(@MappingTarget T entity, DTO dto); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/ResilientLogMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/ResilientLogMapper.java new file mode 100644 index 0000000..ef82554 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/ResilientLogMapper.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.Mapper; + +import com.oguerreiro.resilient.domain.ResilientLog; +import com.oguerreiro.resilient.service.dto.ResilientLogDTO; + +/** + * Mapper for the entity {@link ResilientLog} and its DTO {@link ResilientLogDTO}. + */ +@Mapper(componentModel = "spring") +public interface ResilientLogMapper extends ResilientEntityMapper { + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/UnitConverterMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/UnitConverterMapper.java new file mode 100644 index 0000000..a69754d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/UnitConverterMapper.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.service.dto.UnitConverterDTO; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link UnitConverter} and its DTO {@link UnitConverterDTO}. + */ +@Mapper(componentModel = "spring") +public interface UnitConverterMapper extends ResilientEntityMapper { + @Mapping(target = "fromUnit", source = "fromUnit", qualifiedByName = "unitName") + @Mapping(target = "toUnit", source = "toUnit", qualifiedByName = "unitName") + UnitConverterDTO toDto(UnitConverter s); + + @Named("unitName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "unitType", source = "unitType") + @Mapping(target = "version", source = "version") + UnitDTO toDtoUnitName(Unit unit); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/UnitMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/UnitMapper.java new file mode 100644 index 0000000..e8dd80a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/UnitMapper.java @@ -0,0 +1,23 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.UnitType; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.UnitTypeDTO; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link Unit} and its DTO {@link UnitDTO}. + */ +@Mapper(componentModel = "spring") +public interface UnitMapper extends ResilientEntityMapper { + @Mapping(target = "unitType", source = "unitType", qualifiedByName = "unitTypeName") + UnitDTO toDto(Unit s); + + @Named("unitTypeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + UnitTypeDTO toDtoUnitTypeName(UnitType unitType); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/UnitTypeMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/UnitTypeMapper.java new file mode 100644 index 0000000..a8fd272 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/UnitTypeMapper.java @@ -0,0 +1,16 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import com.oguerreiro.resilient.domain.UnitType; +import com.oguerreiro.resilient.service.dto.UnitTypeDTO; + +/** + * Mapper for the entity {@link UnitType} and its DTO {@link UnitTypeDTO}. + */ +@Mapper(componentModel = "spring") +public interface UnitTypeMapper extends ResilientEntityMapper { + @Mapping(target = "removeUnit", ignore = true) + UnitType toEntity(UnitTypeDTO dto); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/UserMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/UserMapper.java new file mode 100644 index 0000000..a3ba4c6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/UserMapper.java @@ -0,0 +1,150 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.Authority; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import com.oguerreiro.resilient.service.dto.UserDTO; +import java.util.*; +import java.util.stream.Collectors; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.springframework.stereotype.Service; + +/** + * Mapper for the entity {@link User} and its DTO called {@link UserDTO}. + * + * Normal mappers are generated using MapStruct, this one is hand-coded as MapStruct + * support is still in beta, and requires a manual step with an IDE. + */ +@Service +public class UserMapper { + + public List usersToUserDTOs(List users) { + return users.stream().filter(Objects::nonNull).map(this::userToUserDTO).toList(); + } + + public UserDTO userToUserDTO(User user) { + return new UserDTO(user); + } + + public List usersToAdminUserDTOs(List users) { + return users.stream().filter(Objects::nonNull).map(this::userToAdminUserDTO).toList(); + } + + public AdminUserDTO userToAdminUserDTO(User user) { + return new AdminUserDTO(user); + } + + public List userDTOsToUsers(List userDTOs) { + return userDTOs.stream().filter(Objects::nonNull).map(this::userDTOToUser).toList(); + } + + public User userDTOToUser(AdminUserDTO userDTO) { + if (userDTO == null) { + return null; + } else { + User user = new User(); + user.setId(userDTO.getId()); + user.setLogin(userDTO.getLogin()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + user.setEmail(userDTO.getEmail()); + user.setImageUrl(userDTO.getImageUrl()); + user.setCreatedBy(userDTO.getCreatedBy()); + user.setCreatedDate(userDTO.getCreatedDate()); + user.setLastModifiedBy(userDTO.getLastModifiedBy()); + user.setLastModifiedDate(userDTO.getLastModifiedDate()); + user.setActivated(userDTO.isActivated()); + user.setLangKey(userDTO.getLangKey()); + Set authorities = this.authoritiesFromStrings(userDTO.getAuthorities()); + user.setAuthorities(authorities); + return user; + } + } + + private Set authoritiesFromStrings(Set authoritiesAsString) { + Set authorities = new HashSet<>(); + + if (authoritiesAsString != null) { + authorities = authoritiesAsString + .stream() + .map(string -> { + Authority auth = new Authority(); + auth.setName(string); + return auth; + }) + .collect(Collectors.toSet()); + } + + return authorities; + } + + public User userFromId(Long id) { + if (id == null) { + return null; + } + User user = new User(); + user.setId(id); + return user; + } + + @Named("id") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + public UserDTO toDtoId(User user) { + if (user == null) { + return null; + } + UserDTO userDto = new UserDTO(); + userDto.setId(user.getId()); + return userDto; + } + + @Named("idSet") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + public Set toDtoIdSet(Set users) { + if (users == null) { + return Collections.emptySet(); + } + + Set userSet = new HashSet<>(); + for (User userEntity : users) { + userSet.add(this.toDtoId(userEntity)); + } + + return userSet; + } + + @Named("login") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "login", source = "login") + public UserDTO toDtoLogin(User user) { + if (user == null) { + return null; + } + UserDTO userDto = new UserDTO(); + userDto.setId(user.getId()); + userDto.setLogin(user.getLogin()); + return userDto; + } + + @Named("loginSet") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "login", source = "login") + public Set toDtoLoginSet(Set users) { + if (users == null) { + return Collections.emptySet(); + } + + Set userSet = new HashSet<>(); + for (User userEntity : users) { + userSet.add(this.toDtoLogin(userEntity)); + } + + return userSet; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/VariableCategoryMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableCategoryMapper.java new file mode 100644 index 0000000..a25da04 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableCategoryMapper.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.service.dto.VariableCategoryDTO; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; + +/** + * Mapper for the entity {@link VariableCategory} and its DTO + * {@link VariableCategoryDTO}. + */ +@Mapper(componentModel = "spring") +public interface VariableCategoryMapper extends ResilientEntityMapper { + @Mapping(target = "variableScope", source = "variableScope", qualifiedByName = "variableScopeName") + VariableCategoryDTO toDto(VariableCategory s); + + @Named("variableScopeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "code", source = "code") + @Mapping(target = "version", source = "version") + VariableScopeDTO toDtoVariableScopeName(VariableScope variableScope); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/VariableClassMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableClassMapper.java new file mode 100644 index 0000000..20da230 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableClassMapper.java @@ -0,0 +1,26 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.VariableClass; +import com.oguerreiro.resilient.domain.VariableClassType; +import com.oguerreiro.resilient.service.dto.VariableClassDTO; +import com.oguerreiro.resilient.service.dto.VariableClassTypeDTO; + +/** + * Mapper for the entity {@link VariableClass} and its DTO {@link VariableClassDTO}. + */ +@Mapper(componentModel = "spring") +public interface VariableClassMapper extends ResilientEntityMapper { + @Mapping(target = "variableClassType", source = "variableClassType", qualifiedByName = "variableClassTypeName") + VariableClassDTO toDto(VariableClass s); + + @Named("variableClassTypeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + VariableClassTypeDTO toDtoVariableClassTypeName(VariableClassType variableClassType); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/VariableClassTypeMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableClassTypeMapper.java new file mode 100644 index 0000000..e2b74a9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableClassTypeMapper.java @@ -0,0 +1,39 @@ +package com.oguerreiro.resilient.service.mapper; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.VariableClass; +import com.oguerreiro.resilient.domain.VariableClassType; +import com.oguerreiro.resilient.service.dto.VariableClassDTO; +import com.oguerreiro.resilient.service.dto.VariableClassTypeDTO; + +/** + * Mapper for the entity {@link VariableClassType} and its DTO {@link VariableClassTypeDTO}. + */ +@Mapper(componentModel = "spring") +public interface VariableClassTypeMapper extends ResilientEntityMapper { + @Mapping(target = "variableClasses", source = "variableClasses", qualifiedByName = "variableClassNameSet") + VariableClassTypeDTO toDto(VariableClassType s); + + @Mapping(target = "removeVariableClass", ignore = true) + VariableClassType toEntity(VariableClassTypeDTO variableClassTypeDTO); + + @Named("variableClassPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "code", source = "code") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableClassDTO toDtoVariableClassPropertyName(VariableClass variableClass); + + @Named("variableClassNameSet") + default Set toDtoVariableClassPropertyNameSet(Set variableClass) { + return variableClass.stream().map(this::toDtoVariableClassPropertyName).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/VariableMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableMapper.java new file mode 100644 index 0000000..f8af6d1 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableMapper.java @@ -0,0 +1,80 @@ +package com.oguerreiro.resilient.service.mapper; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.domain.VariableClassType; +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.domain.VariableUnits; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.VariableCategoryDTO; +import com.oguerreiro.resilient.service.dto.VariableClassTypeDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; +import com.oguerreiro.resilient.service.dto.VariableUnitsDTO; + +/** + * Mapper for the entity {@link Variable} and its DTO {@link VariableDTO}. + */ +@Mapper(componentModel = "spring") +public interface VariableMapper extends ResilientEntityMapper { + @Mapping(target = "baseUnit", source = "baseUnit", qualifiedByName = "unitName") + @Mapping(target = "variableScope", source = "variableScope", qualifiedByName = "variableScopeName") + @Mapping(target = "variableCategory", source = "variableCategory", qualifiedByName = "variableCategoryName") + @Mapping(target = "variableClassType", source = "variableClassType", qualifiedByName = "variableClassTypeName") + @Mapping(target = "variableUnits", source = "variableUnits", qualifiedByName = "variableUnitsNameSet") + VariableDTO toDto(Variable s); + + @Named("unitName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "unitType", source = "unitType") + @Mapping(target = "version", source = "version") + UnitDTO toDtoUnitName(Unit unit); + + @Named("variableScopeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableScopeDTO toDtoVariableScopeName(VariableScope variableScope); + + @Named("variableCategoryName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableCategoryDTO toDtoVariableCategoryName(VariableCategory variableCategory); + + @Named("variableClassTypeName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + @Mapping(target = "version", source = "version") + VariableClassTypeDTO toDtoVariableClassTypeName(VariableClassType variableClassType); + + @Mapping(target = "removeVariableUnits", ignore = true) + Variable toEntity(VariableDTO variableDTO); + + @Named("variableUnitsPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "unit", source = "unit") + @Mapping(target = "unitConverter", source = "unitConverter") + @Mapping(target = "version", source = "version") + VariableUnitsDTO toDtoVariableUnitsPropertyName(VariableUnits variableUnits); + + @Named("variableUnitsNameSet") + default Set toDtoVariableUnitsPropertyNameSet(Set variableUnits) { + return variableUnits.stream().map(this::toDtoVariableUnitsPropertyName).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/VariableScopeMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableScopeMapper.java new file mode 100644 index 0000000..bd11ff2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableScopeMapper.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.service.mapper; + +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; +import org.mapstruct.*; + +/** + * Mapper for the entity {@link VariableScope} and its DTO {@link VariableScopeDTO}. + */ +@Mapper(componentModel = "spring") +public interface VariableScopeMapper extends ResilientEntityMapper {} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/VariableUnitsMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableUnitsMapper.java new file mode 100644 index 0000000..67d1500 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/VariableUnitsMapper.java @@ -0,0 +1,44 @@ +package com.oguerreiro.resilient.service.mapper; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.VariableUnits; +import com.oguerreiro.resilient.service.dto.UnitConverterDTO; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; +import com.oguerreiro.resilient.service.dto.VariableUnitsDTO; + +/** + * Mapper for the entity {@link VariableUnits} and its DTO {@link VariableUnitsDTO}. + */ +@Mapper(componentModel = "spring") +public interface VariableUnitsMapper extends ResilientEntityMapper { + @Mapping(target = "variable", source = "variable", qualifiedByName = "variableName") + @Mapping(target = "unit", source = "unit", qualifiedByName = "unitName") + @Mapping(target = "unitConverter", source = "unitConverter", qualifiedByName = "unitConverterName") + VariableUnitsDTO toDto(VariableUnits s); + + @Named("variableName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + VariableDTO toDtoVariableName(Variable variable); + + @Named("unitName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + UnitDTO toDtoUnitName(Unit unit); + + @Named("unitConverterName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "name", source = "name") + UnitConverterDTO toDtoUnitConverterName(UnitConverter unitConverter); +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/package-info.java b/src/main/java/com/oguerreiro/resilient/service/mapper/package-info.java new file mode 100644 index 0000000..a99a993 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * Data transfer objects mappers. + */ +package com.oguerreiro.resilient.service.mapper; diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/security/SecurityGroupMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/security/SecurityGroupMapper.java new file mode 100644 index 0000000..efcf894 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/security/SecurityGroupMapper.java @@ -0,0 +1,38 @@ +package com.oguerreiro.resilient.service.mapper.security; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import com.oguerreiro.resilient.security.resillient.SecurityGroup; +import com.oguerreiro.resilient.security.resillient.SecurityGroupPermission; +import com.oguerreiro.resilient.service.dto.security.SecurityGroupDTO; +import com.oguerreiro.resilient.service.dto.security.SecurityGroupPermissionDTO; +import com.oguerreiro.resilient.service.mapper.ResilientEntityMapper; + +/** + * Mapper for the entity {@link SecurityGroup} and its DTO {@link SecurityGroupDTO}. + */ +@Mapper(componentModel = "spring") +public interface SecurityGroupMapper extends ResilientEntityMapper { + // Maps only SecurityGroup, without any other relation + @Mapping(target = "securityGroupPermissions", ignore = true) + SecurityGroupDTO toDto(SecurityGroup s); + + @Mapping(target = "securityGroup", ignore = true) + SecurityGroupPermissionDTO toDto(SecurityGroupPermission s); + + /* + @Named("securityGroupPermissionPropertyName") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "version", source = "version") + SecurityGroupPermissionDTO toDtoSecurityGroupPermissionPropertyName(SecurityGroupPermission securityGroupPermission); + + @Named("securityGroupPermissionNameSet") + default List toDtoSecurityGroupPermissionPropertyNameSet( + List securityGroupPermission) { + return securityGroupPermission.stream().map(this::toDtoSecurityGroupPermissionPropertyName).collect( + Collectors.toList()); + } + */ +} diff --git a/src/main/java/com/oguerreiro/resilient/service/mapper/security/SecurityResourceMapper.java b/src/main/java/com/oguerreiro/resilient/service/mapper/security/SecurityResourceMapper.java new file mode 100644 index 0000000..32ec16f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/mapper/security/SecurityResourceMapper.java @@ -0,0 +1,15 @@ +package com.oguerreiro.resilient.service.mapper.security; + +import org.mapstruct.Mapper; + +import com.oguerreiro.resilient.security.resillient.SecurityResource; +import com.oguerreiro.resilient.service.dto.security.SecurityResourceDTO; +import com.oguerreiro.resilient.service.mapper.ResilientEntityMapper; + +/** + * Mapper for the entity {@link SecurityResource} and its DTO {@link SecurityResourceDTO}. + */ +@Mapper(componentModel = "spring") +public interface SecurityResourceMapper extends ResilientEntityMapper { + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/package-info.java b/src/main/java/com/oguerreiro/resilient/service/package-info.java new file mode 100644 index 0000000..0d47a48 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/package-info.java @@ -0,0 +1,4 @@ +/** + * Service layer. + */ +package com.oguerreiro.resilient.service; diff --git a/src/main/java/com/oguerreiro/resilient/service/query/AbstractQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/AbstractQueryService.java new file mode 100644 index 0000000..4203971 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/AbstractQueryService.java @@ -0,0 +1,133 @@ +package com.oguerreiro.resilient.service.query; + +import java.io.Serializable; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.repository.ResilientJpaRepository; +import com.oguerreiro.resilient.service.criteria.AbstractCriteria; +import com.oguerreiro.resilient.service.dto.EmissionFactorDTO; +import com.oguerreiro.resilient.service.mapper.ResilientEntityMapper; + +import tech.jhipster.service.QueryService; + +/** + * + * @param Entity Domain class type + * @param Entity Domain DTO class type, for + * @param Criteria + */ +@Transactional(readOnly = true) +public abstract class AbstractQueryService, DTO, ID extends Serializable, C extends AbstractCriteria> + extends QueryService { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + /** + * ATTENTION
+ * Use this constant to identify the id attribute in a join relation sprcification. + * Normal usage, for a join relation between InputData and Unit, would be: + * + *

+   * 
+  specification = specification.and(
+    buildSpecification(
+        criteria.getSourceUnitId(),
+        root -> root.join(InputData_.sourceUnit, JoinType.LEFT).get(Unit_.id)
+    )
+  );
+   * 
+   * 
+ * + * But this will throw a NullPointerException. Because Unit_id is NOT defined in the generated classe Unit_, but in + * the extended AbstractResilientLongIdEntity_. Hibernate doesn't manage weel this hierarchy and doesn't find it, + * thus, throwing the NullPointerException. + * + * The solution is to use the name of the property as string. And thats what this constant is for. + * The corrected code is: + * + *
+   * 
+  specification = specification.and(
+    buildSpecification(
+        criteria.getSourceUnitId(),
+        root -> root.join(InputData_.sourceUnit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME)
+    )
+  );
+   * 
+   * 
+ */ + public static final String ID_ATTRIBUTE_NAME = "id"; + + private final ResilientEntityMapper domainMapper; + + private final ResilientJpaRepository domainRepository; + + protected abstract Specification createSpecification(C criteria); + + /** + * Overridable method, for cases theres a need for a more complex query created in PJARepository. + * The default is domainRepository.findAll(specification) + * + * @param specification + * @return + */ + protected List find(Specification specification) { + return domainRepository.findAll(specification); + } + + protected List find(Specification specification, Sort sort) { + return domainRepository.findAll(specification, sort); + } + + protected AbstractQueryService(ResilientJpaRepository domainRepository, ResilientEntityMapper domainMapper) { + this.domainRepository = domainRepository; + this.domainMapper = domainMapper; + } + + /** + * Return a {@link List} of {@link EmissionFactorDTO} which matches the criteria from + * the database. + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the matching entities. + */ + @Transactional(readOnly = true) + public List findByCriteria(C criteria) { + log.debug("find by criteria : {}", criteria); + + return findByCriteria(criteria, Sort.unsorted()); + } + + @Transactional(readOnly = true) + public List findByCriteria(C criteria, Sort sort) { + log.debug("find by criteria : {}", criteria, sort); + + final Specification specification = createSpecification(criteria); + List list = this.find(specification, sort); + List dtos = this.domainMapper.toDto(list); + + return dtos; + } + + /** + * Return the number of matching entities in the database. + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the number of matching entities. + */ + @Transactional(readOnly = true) + public long countByCriteria(C criteria) { + log.debug("count by criteria : {}", criteria); + final Specification specification = createSpecification(criteria); + return domainRepository.count(specification); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/EmissionFactorQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/EmissionFactorQueryService.java new file mode 100644 index 0000000..5bf0098 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/EmissionFactorQueryService.java @@ -0,0 +1,85 @@ +package com.oguerreiro.resilient.service.query; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +// for static metamodels +import com.oguerreiro.resilient.domain.EmissionFactor; +import com.oguerreiro.resilient.domain.EmissionFactor_; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.service.criteria.EmissionFactorCriteria; +import com.oguerreiro.resilient.service.dto.EmissionFactorDTO; +import com.oguerreiro.resilient.service.mapper.EmissionFactorMapper; + +import jakarta.persistence.criteria.JoinType; + +/** + * Service for executing complex queries for {@link EmissionFactor} entities in the + * database. The main input is a {@link EmissionFactorCriteria} which gets converted + * to {@link Specification}, in a way that all the filters must apply. It + * returns a {@link List} of {@link EmissionFactorDTO} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class EmissionFactorQueryService + extends AbstractQueryService { + + public EmissionFactorQueryService(EmissionFactorRepository emissionFactorRepository, + EmissionFactorMapper emissionFactorMapper) { + super(emissionFactorRepository, emissionFactorMapper); + } + + /** + * Function to convert {@link EmissionFactorCriteria} to a {@link Specification} + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(EmissionFactorCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), EmissionFactor_.id)); + } + if (criteria.getValue() != null) { + specification = specification.and(buildRangeSpecification(criteria.getValue(), EmissionFactor_.value)); + } + if (criteria.getYear() != null) { + specification = specification.and(buildRangeSpecification(criteria.getYear(), EmissionFactor_.year)); + } + if (criteria.getCode() != null) { + specification = specification.and(buildStringSpecification(criteria.getCode(), EmissionFactor_.code)); + } + if (criteria.getName() != null) { + specification = specification.and(buildStringSpecification(criteria.getName(), EmissionFactor_.name)); + } + if (criteria.getUnit() != null) { + specification = specification.and(buildStringSpecification(criteria.getUnit(), EmissionFactor_.unit)); + } + if (criteria.getSource() != null) { + specification = specification.and(buildStringSpecification(criteria.getSource(), EmissionFactor_.source)); + } + if (criteria.getComments() != null) { + specification = specification.and(buildStringSpecification(criteria.getComments(), EmissionFactor_.comments)); + } + if (criteria.getVariableScopeId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableScopeId(), + root -> root.join(EmissionFactor_.variableScope, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableCategoryId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableCategoryId(), + root -> root.join(EmissionFactor_.variableCategory, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + } + return specification; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/EmissionFactorService.java b/src/main/java/com/oguerreiro/resilient/service/query/EmissionFactorService.java new file mode 100644 index 0000000..965da42 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/EmissionFactorService.java @@ -0,0 +1,106 @@ +package com.oguerreiro.resilient.service.query; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.EmissionFactor; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.service.AbstractResilientService; +import com.oguerreiro.resilient.service.dto.EmissionFactorDTO; +import com.oguerreiro.resilient.service.mapper.EmissionFactorMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.domain.EmissionFactor}. + */ +@Service +@Transactional +public class EmissionFactorService extends AbstractResilientService { + private final EmissionFactorRepository emissionFactorRepository; + private final EmissionFactorMapper emissionFactorMapper; + + public EmissionFactorService(EmissionFactorRepository emissionFactorRepository, + EmissionFactorMapper emissionFactorMapper) { + super(EmissionFactor.class, emissionFactorRepository, emissionFactorMapper); + this.emissionFactorRepository = emissionFactorRepository; + this.emissionFactorMapper = emissionFactorMapper; + } + + /** + * Get one emissionFactor by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Override + @Transactional(readOnly = true) + public Optional findOne(Long id) { + log.debug("[OVERRIDE] Request to findOne : {}", id); + return findOneWithEagerRelationships(id); + } + + @Transactional(readOnly = true) + public byte[] export(Integer year, Long scopeId, Long categoryId) { + //Build a Workbook + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("Export"); + + // Header row + int c = 0; + Row headerRow = sheet.createRow(0); + headerRow.createCell(c++).setCellValue("Ano"); + headerRow.createCell(c++).setCellValue("Âmbito"); + headerRow.createCell(c++).setCellValue("Categpria"); + headerRow.createCell(c++).setCellValue("Nome"); + headerRow.createCell(c++).setCellValue("Valor"); + headerRow.createCell(c++).setCellValue("Unidade"); + headerRow.createCell(c++).setCellValue("Fonte"); + + // Get data with a stream (query cursor) + try (Stream stream = this.emissionFactorRepository.streamAll(year, scopeId, categoryId)) { + // Loop data + stream.forEach(emissionFactor -> { + int cc = 0; + // Write row + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + row.createCell(cc++).setCellValue(emissionFactor.getYear().toString()); + row.createCell(cc++).setCellValue(emissionFactor.getVariableScope().getName()); + row.createCell(cc++).setCellValue(emissionFactor.getVariableCategory().getName()); + row.createCell(cc++).setCellValue(emissionFactor.getName()); + row.createCell(cc++).setCellValue(emissionFactor.getValue().setScale(8).toPlainString()); + row.createCell(cc++).setCellValue(emissionFactor.getUnit()); + row.createCell(cc++).setCellValue(emissionFactor.getSource()); + }); + } + + workbook.write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to generate Excel file", e); + } + } + + /** + * Get all EmissionFactor wrapped in a EmissionFactorDTO + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAllBySearch(Integer year, Long scopeId, Long categoryId) { + log.debug("Request to get all EmissionFactor's."); + + return emissionFactorRepository.findAllBySearch(year, scopeId, categoryId).stream().map( + this.emissionFactorMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/InputDataQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/InputDataQueryService.java new file mode 100644 index 0000000..791e95a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/InputDataQueryService.java @@ -0,0 +1,182 @@ +package com.oguerreiro.resilient.service.query; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +// for static metamodels +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.InputData_; +import com.oguerreiro.resilient.domain.Variable_; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.service.criteria.InputDataCriteria; +import com.oguerreiro.resilient.service.dto.InputDataDTO; +import com.oguerreiro.resilient.service.mapper.InputDataMapper; + +import jakarta.persistence.criteria.JoinType; + +/** + * Service for executing complex queries for {@link InputData} entities in the + * database. The main input is a {@link InputDataCriteria} which gets converted + * to {@link Specification}, in a way that all the filters must apply. It + * returns a {@link List} of {@link InputDataDTO} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class InputDataQueryService extends AbstractQueryService { + + public InputDataQueryService(InputDataRepository inputDataRepository, InputDataMapper inputDataMapper) { + super(inputDataRepository, inputDataMapper); + } + + /** + * Function to convert {@link InputDataCriteria} to a {@link Specification} + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(InputDataCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), InputData_.id)); + } + if (criteria.getSourceValue() != null) { + specification = specification.and(buildRangeSpecification(criteria.getSourceValue(), InputData_.sourceValue)); + } + if (criteria.getVariableValue() != null) { + specification = specification.and( + buildRangeSpecification(criteria.getVariableValue(), InputData_.variableValue)); + } + if (criteria.getImputedValue() != null) { + specification = specification.and(buildRangeSpecification(criteria.getImputedValue(), InputData_.imputedValue)); + } + if (criteria.getSourceType() != null) { + specification = specification.and(buildSpecification(criteria.getSourceType(), InputData_.sourceType)); + } + if (criteria.getDataDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getDataDate(), InputData_.dataDate)); + } + if (criteria.getChangeDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getChangeDate(), InputData_.changeDate)); + } + if (criteria.getChangeUsername() != null) { + specification = specification.and( + buildStringSpecification(criteria.getChangeUsername(), InputData_.changeUsername)); + } + if (criteria.getDataSource() != null) { + specification = specification.and(buildStringSpecification(criteria.getDataSource(), InputData_.dataSource)); + } + if (criteria.getDataUser() != null) { + specification = specification.and(buildStringSpecification(criteria.getDataUser(), InputData_.dataUser)); + } + if (criteria.getDataComments() != null) { + specification = specification.and( + buildStringSpecification(criteria.getDataComments(), InputData_.dataComments)); + } + if (criteria.getCreationDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getCreationDate(), InputData_.creationDate)); + } + if (criteria.getCreationUsername() != null) { + specification = specification.and( + buildStringSpecification(criteria.getCreationUsername(), InputData_.creationUsername)); + } + if (criteria.getVersion() != null) { + specification = specification.and(buildRangeSpecification(criteria.getVersion(), InputData_.version)); + } + if (criteria.getInputDataId() != null) { + specification = specification.and(buildSpecification(criteria.getInputDataId(), + root -> root.join(InputData_.inputData, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableId(), + root -> root.join(InputData_.variable, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableNameOrCode() != null) { + //@formatter:off + specification = specification.and( + buildSpecification( + criteria.getVariableNameOrCode(), root -> root.join(InputData_.variable, JoinType.LEFT).get(Variable_.code) + ).or( + buildSpecification( + criteria.getVariableNameOrCode(), root -> root.join(InputData_.variable, JoinType.LEFT).get(Variable_.name)) + ) + ); + //@formatter:on + } + if (criteria.getVariableScopeId() != null) { + //@formatter:off + specification = specification.and(buildSpecification(criteria.getVariableScopeId(), + root -> root + .join(InputData_.variable, JoinType.LEFT) + .join(Variable_.variableScope, JoinType.LEFT) + .get(ID_ATTRIBUTE_NAME) + ) + ); + //@formatter:on + } + if (criteria.getVariableCategoryId() != null) { + //@formatter:off + specification = specification.and(buildSpecification(criteria.getVariableCategoryId(), + root -> root + .join(InputData_.variable, JoinType.LEFT) + .join(Variable_.variableCategory, JoinType.LEFT) + .get(ID_ATTRIBUTE_NAME) + ) + ); + //@formatter:on + } + if (criteria.getVariableClassNameOrCode() != null) { + //@formatter:off + specification = specification.and( + buildStringSpecification( + criteria.getVariableClassNameOrCode(), InputData_.variableClassCode + ).or( + buildStringSpecification(criteria.getVariableClassNameOrCode(), InputData_.variableClassName) + ) + ); + //@formatter:on + + } + + if (criteria.getPeriodId() != null) { + // @formatter:off + specification = specification.and( + buildSpecification( + criteria.getPeriodId(), root -> root.join(InputData_.period, JoinType.LEFT).get(ID_ATTRIBUTE_NAME) + ) + ); + // @formatter:on + } + if (criteria.getSourceUnitId() != null) { + specification = specification.and(buildSpecification(criteria.getSourceUnitId(), + root -> root.join(InputData_.sourceUnit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getUnitId() != null) { + specification = specification.and(buildSpecification(criteria.getUnitId(), + root -> root.join(InputData_.unit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getOwnerId() != null) { + specification = specification.and(buildSpecification(criteria.getOwnerId(), + root -> root.join(InputData_.owner, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getSourceInputDataId() != null) { + specification = specification.and(buildSpecification(criteria.getSourceInputDataId(), + root -> root.join(InputData_.sourceInputData, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getSourceInputDataUploadId() != null) { + specification = specification.and(buildSpecification(criteria.getSourceInputDataUploadId(), + root -> root.join(InputData_.sourceInputDataUpload, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + + } + return specification; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/InventoryQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/InventoryQueryService.java new file mode 100644 index 0000000..2aaabd8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/InventoryQueryService.java @@ -0,0 +1,136 @@ +package com.oguerreiro.resilient.service.query; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +// for static metamodels +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.InputData_; +import com.oguerreiro.resilient.domain.Variable_; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.service.criteria.InputDataCriteria; +import com.oguerreiro.resilient.service.dto.InputDataDTO; +import com.oguerreiro.resilient.service.mapper.InputDataMapper; + +import jakarta.persistence.criteria.JoinType; + +/** + * Service for executing complex queries for {@link InputData} entities in the + * database. The main input is a {@link InputDataCriteria} which gets converted + * to {@link Specification}, in a way that all the filters must apply. It + * returns a {@link List} of {@link InputDataDTO} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class InventoryQueryService extends AbstractQueryService { + + public InventoryQueryService(InputDataRepository inputDataRepository, InputDataMapper inputDataMapper) { + super(inputDataRepository, inputDataMapper); + } + + /** + * Function to convert {@link InputDataCriteria} to a {@link Specification} + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(InputDataCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), InputData_.id)); + } + if (criteria.getSourceValue() != null) { + specification = specification.and(buildRangeSpecification(criteria.getSourceValue(), InputData_.sourceValue)); + } + if (criteria.getVariableValue() != null) { + specification = specification.and( + buildRangeSpecification(criteria.getVariableValue(), InputData_.variableValue)); + } + if (criteria.getImputedValue() != null) { + specification = specification.and(buildRangeSpecification(criteria.getImputedValue(), InputData_.imputedValue)); + } + if (criteria.getSourceType() != null) { + specification = specification.and(buildSpecification(criteria.getSourceType(), InputData_.sourceType)); + } + if (criteria.getDataDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getDataDate(), InputData_.dataDate)); + } + if (criteria.getChangeDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getChangeDate(), InputData_.changeDate)); + } + if (criteria.getChangeUsername() != null) { + specification = specification.and( + buildStringSpecification(criteria.getChangeUsername(), InputData_.changeUsername)); + } + if (criteria.getDataSource() != null) { + specification = specification.and(buildStringSpecification(criteria.getDataSource(), InputData_.dataSource)); + } + if (criteria.getDataUser() != null) { + specification = specification.and(buildStringSpecification(criteria.getDataUser(), InputData_.dataUser)); + } + if (criteria.getDataComments() != null) { + specification = specification.and( + buildStringSpecification(criteria.getDataComments(), InputData_.dataComments)); + } + if (criteria.getCreationDate() != null) { + specification = specification.and(buildRangeSpecification(criteria.getCreationDate(), InputData_.creationDate)); + } + if (criteria.getCreationUsername() != null) { + specification = specification.and( + buildStringSpecification(criteria.getCreationUsername(), InputData_.creationUsername)); + } + if (criteria.getVersion() != null) { + specification = specification.and(buildRangeSpecification(criteria.getVersion(), InputData_.version)); + } + if (criteria.getInputDataId() != null) { + specification = specification.and(buildSpecification(criteria.getInputDataId(), + root -> root.join(InputData_.inputData, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableId(), + root -> root.join(InputData_.variable, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableNameOrCode() != null) { + // TODO : make a OR and search both Variable.code and Variable.name + specification = specification.and(buildSpecification(criteria.getVariableNameOrCode(), + root -> root.join(InputData_.variable, JoinType.LEFT).get(Variable_.code)).or( + buildSpecification(criteria.getVariableNameOrCode(), + root -> root.join(InputData_.variable, JoinType.LEFT).get(Variable_.name)))); + } + if (criteria.getPeriodId() != null) { + specification = specification.and(buildSpecification(criteria.getPeriodId(), + root -> root.join(InputData_.period, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getSourceUnitId() != null) { + specification = specification.and(buildSpecification(criteria.getSourceUnitId(), + root -> root.join(InputData_.sourceUnit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getUnitId() != null) { + specification = specification.and(buildSpecification(criteria.getUnitId(), + root -> root.join(InputData_.unit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getOwnerId() != null) { + specification = specification.and(buildSpecification(criteria.getOwnerId(), + root -> root.join(InputData_.owner, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getSourceInputDataId() != null) { + specification = specification.and(buildSpecification(criteria.getSourceInputDataId(), + root -> root.join(InputData_.sourceInputData, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getSourceInputDataUploadId() != null) { + specification = specification.and(buildSpecification(criteria.getSourceInputDataUploadId(), + root -> root.join(InputData_.sourceInputDataUpload, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + } + return specification; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/OutputDataQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/OutputDataQueryService.java new file mode 100644 index 0000000..2ee7201 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/OutputDataQueryService.java @@ -0,0 +1,87 @@ +package com.oguerreiro.resilient.service.query; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +// for static metamodels +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.domain.OutputData_; +import com.oguerreiro.resilient.domain.Variable_; +import com.oguerreiro.resilient.repository.OutputDataRepository; +import com.oguerreiro.resilient.service.criteria.OutputDataCriteria; +import com.oguerreiro.resilient.service.dto.OutputDataDTO; +import com.oguerreiro.resilient.service.mapper.OutputDataMapper; + +import jakarta.persistence.criteria.JoinType; + +/** + * Service for executing complex queries for {@link OutputData} entities in the + * database. The main input is a {@link OutputDataCriteria} which gets converted + * to {@link Specification}, in a way that all the filters must apply. It + * returns a {@link List} of {@link OutputDataDTO} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class OutputDataQueryService extends AbstractQueryService { + + public OutputDataQueryService(OutputDataRepository outputDataRepository, OutputDataMapper outputDataMapper) { + super(outputDataRepository, outputDataMapper); + } + + /** + * Function to convert {@link OutputDataCriteria} to a {@link Specification} + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(OutputDataCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), OutputData_.id)); + } + if (criteria.getVariableId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableId(), + root -> root.join(OutputData_.variable, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableNameOrCode() != null) { + // TODO : make a OR and search both Variable.code and Variable.name + specification = specification.and(buildSpecification(criteria.getVariableNameOrCode(), + root -> root.join(OutputData_.variable, JoinType.LEFT).get(Variable_.code)).or( + buildSpecification(criteria.getVariableNameOrCode(), + root -> root.join(OutputData_.variable, JoinType.LEFT).get(Variable_.name)))); + } + if (criteria.getVariableScopeId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableScopeId(), + root -> root.join(OutputData_.variable, JoinType.LEFT).join(Variable_.variableScope, JoinType.LEFT).get( + ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableCategoryId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableCategoryId(), + root -> root.join(OutputData_.variable, JoinType.LEFT).join(Variable_.variableCategory, JoinType.LEFT).get( + ID_ATTRIBUTE_NAME))); + } + if (criteria.getPeriodId() != null) { + specification = specification.and(buildSpecification(criteria.getPeriodId(), + root -> root.join(OutputData_.period, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getPeriodVersionId() != null) { + specification = specification.and(buildSpecification(criteria.getPeriodVersionId(), + root -> root.join(OutputData_.periodVersion, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getOwnerId() != null) { + specification = specification.and(buildSpecification(criteria.getOwnerId(), + root -> root.join(OutputData_.owner, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + } + return specification; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/UnitConverterQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/UnitConverterQueryService.java new file mode 100644 index 0000000..c3cbc78 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/UnitConverterQueryService.java @@ -0,0 +1,71 @@ +package com.oguerreiro.resilient.service.query; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +// for static metamodels +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.domain.UnitConverter_; +import com.oguerreiro.resilient.repository.UnitConverterRepository; +import com.oguerreiro.resilient.service.criteria.UnitConverterCriteria; +import com.oguerreiro.resilient.service.dto.UnitConverterDTO; +import com.oguerreiro.resilient.service.mapper.UnitConverterMapper; + +import jakarta.persistence.criteria.JoinType; + +/** + * Service for executing complex queries for {@link UnitConverter} entities in the + * database. The main input is a {@link UnitConverterCriteria} which gets converted + * to {@link Specification}, in a way that all the filters must apply. It + * returns a {@link List} of {@link UnitConverterDTO} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class UnitConverterQueryService + extends AbstractQueryService { + + public UnitConverterQueryService(UnitConverterRepository unitConverterRepository, + UnitConverterMapper unitConverterMapper) { + super(unitConverterRepository, unitConverterMapper); + } + + /** + * Function to convert {@link UnitConverterCriteria} to a {@link Specification} + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(UnitConverterCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), UnitConverter_.id)); + } + if (criteria.getDescription() != null) { + specification = specification.and( + buildStringSpecification(criteria.getDescription(), UnitConverter_.description)); + } + if (criteria.getName() != null) { + specification = specification.and(buildStringSpecification(criteria.getName(), UnitConverter_.name)); + } + if (criteria.getFromUnitId() != null) { + specification = specification.and(buildSpecification(criteria.getFromUnitId(), + root -> root.join(UnitConverter_.fromUnit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getToUnitId() != null) { + specification = specification.and(buildSpecification(criteria.getToUnitId(), + root -> root.join(UnitConverter_.toUnit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + + } + return specification; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/UnitQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/UnitQueryService.java new file mode 100644 index 0000000..9b91e65 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/UnitQueryService.java @@ -0,0 +1,70 @@ +package com.oguerreiro.resilient.service.query; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.Unit_; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.service.criteria.UnitConverterCriteria; +import com.oguerreiro.resilient.service.criteria.UnitCriteria; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.mapper.UnitMapper; + +import jakarta.persistence.criteria.JoinType; + +/** + * Service for executing complex queries for {@link Unit} entities in the + * database. The main input is a {@link UnitCriteria} which gets converted + * to {@link Specification}, in a way that all the filters must apply. It + * returns a {@link List} of {@link UnitDTO} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class UnitQueryService extends AbstractQueryService { + + public UnitQueryService(UnitRepository unitRepository, UnitMapper unitMapper) { + super(unitRepository, unitMapper); + } + + /** + * Function to convert {@link UnitConverterCriteria} to a {@link Specification} + * + * @param criteria The object which holds all the filters, which the entities + * should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(UnitCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), Unit_.id)); + } + if (criteria.getCode() != null) { + specification = specification.and(buildStringSpecification(criteria.getCode(), Unit_.code)); + } + if (criteria.getName() != null) { + specification = specification.and(buildStringSpecification(criteria.getName(), Unit_.name)); + } + if (criteria.getSymbol() != null) { + specification = specification.and(buildStringSpecification(criteria.getSymbol(), Unit_.symbol)); + } + if (criteria.getDescription() != null) { + specification = specification.and(buildStringSpecification(criteria.getDescription(), Unit_.description)); + } + if (criteria.getUnitTypeId() != null) { + specification = specification.and(buildSpecification(criteria.getUnitTypeId(), + root -> root.join(Unit_.unitType, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + + } + return specification; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/query/VariableQueryService.java b/src/main/java/com/oguerreiro/resilient/service/query/VariableQueryService.java new file mode 100644 index 0000000..be03083 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/query/VariableQueryService.java @@ -0,0 +1,96 @@ +package com.oguerreiro.resilient.service.query; + +import java.util.List; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.Variable_; +import com.oguerreiro.resilient.repository.VariableRepository; +import com.oguerreiro.resilient.service.criteria.VariableCriteria; +import com.oguerreiro.resilient.service.dto.VariableDTO; +import com.oguerreiro.resilient.service.mapper.VariableMapper; + +import jakarta.persistence.criteria.JoinType; + +/** + * Service for executing complex queries for {@link Variable} entities in the database. + * The main input is a {@link VariableCriteria} which gets converted to {@link Specification}, + * in a way that all the filters must apply. + * It returns a {@link List} of {@link VariableDTO} which fulfills the criteria. + */ +@Service +@Transactional(readOnly = true) +public class VariableQueryService extends AbstractQueryService { + + public VariableQueryService(VariableRepository variableRepository, VariableMapper variableMapper) { + super(variableRepository, variableMapper); + } + + /** + * Function to convert {@link VariableCriteria} to a {@link Specification} + * + * @param criteria The object which holds all the filters, which the entities should match. + * @return the matching {@link Specification} of the entity. + */ + protected Specification createSpecification(VariableCriteria criteria) { + Specification specification = Specification.where(null); + if (criteria != null) { + // This has to be called first, because the distinct method returns null + if (criteria.getDistinct() != null) { + specification = specification.and(distinct(criteria.getDistinct())); + } + if (criteria.getId() != null) { + specification = specification.and(buildRangeSpecification(criteria.getId(), Variable_.id)); + } + if (criteria.getCode() != null) { + specification = specification.and(buildStringSpecification(criteria.getCode(), Variable_.code)); + } + if (criteria.getVariableIndex() != null) { + specification = specification.and( + buildRangeSpecification(criteria.getVariableIndex(), Variable_.variableIndex)); + } + if (criteria.getName() != null) { + specification = specification.and(buildStringSpecification(criteria.getName(), Variable_.name)); + } + if (criteria.getDescription() != null) { + specification = specification.and(buildStringSpecification(criteria.getDescription(), Variable_.description)); + } + if (criteria.getInput() != null) { + specification = specification.and(buildSpecification(criteria.getInput(), Variable_.input)); + } + if (criteria.getInputMode() != null) { + specification = specification.and(buildSpecification(criteria.getInputMode(), Variable_.inputMode)); + } + if (criteria.getOutput() != null) { + specification = specification.and(buildSpecification(criteria.getOutput(), Variable_.output)); + } + if (criteria.getOutputFormula() != null) { + specification = specification.and( + buildStringSpecification(criteria.getOutputFormula(), Variable_.outputFormula)); + } + if (criteria.getVersion() != null) { + specification = specification.and(buildRangeSpecification(criteria.getVersion(), Variable_.version)); + } + if (criteria.getVariableUnitsId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableUnitsId(), + root -> root.join(Variable_.variableUnits, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getBaseUnitId() != null) { + specification = specification.and(buildSpecification(criteria.getBaseUnitId(), + root -> root.join(Variable_.baseUnit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableScopeId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableScopeId(), + root -> root.join(Variable_.variableScope, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + if (criteria.getVariableCategoryId() != null) { + specification = specification.and(buildSpecification(criteria.getVariableCategoryId(), + root -> root.join(Variable_.variableCategory, JoinType.LEFT).get(ID_ATTRIBUTE_NAME))); + } + } + return specification; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/security/SecurityGroupService.java b/src/main/java/com/oguerreiro/resilient/service/security/SecurityGroupService.java new file mode 100644 index 0000000..1827b05 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/security/SecurityGroupService.java @@ -0,0 +1,43 @@ +package com.oguerreiro.resilient.service.security; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.repository.security.SecurityGroupPermissionRepository; +import com.oguerreiro.resilient.repository.security.SecurityGroupRepository; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; +import com.oguerreiro.resilient.service.AbstractResilientService; +import com.oguerreiro.resilient.service.dto.security.SecurityGroupDTO; +import com.oguerreiro.resilient.service.dto.security.SecurityGroupPermissionDTO; +import com.oguerreiro.resilient.service.mapper.security.SecurityGroupMapper; + +import jakarta.persistence.EntityManager; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.security.SecurityGroup}. + */ +@Service +@Transactional +public class SecurityGroupService extends AbstractResilientService { + + private final SecurityGroupPermissionRepository securityGroupPermissionRepository; + private final SecurityGroupMapper securityGroupMapper; + + public SecurityGroupService(SecurityGroupRepository securityGroupRepository, SecurityGroupMapper securityGroupMapper, + EntityManager entityManager, SecurityGroupPermissionRepository securityGroupPermissionRepository) { + super(SecurityGroup.class, securityGroupRepository, securityGroupMapper); + this.securityGroupPermissionRepository = securityGroupPermissionRepository; + this.securityGroupMapper = securityGroupMapper; + } + + @Transactional(readOnly = true) + public List findPermissionsFor(Long securityGroupId) { + log.debug("Request to findPermissionsFor " + this.getClass().getSimpleName() + "'s."); + return securityGroupPermissionRepository.findBySecurityGroupId(securityGroupId).stream().map( + securityGroupMapper::toDto).collect(Collectors.toCollection(LinkedList::new)); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/service/security/SecurityResourceService.java b/src/main/java/com/oguerreiro/resilient/service/security/SecurityResourceService.java new file mode 100644 index 0000000..69f3820 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/service/security/SecurityResourceService.java @@ -0,0 +1,25 @@ +package com.oguerreiro.resilient.service.security; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oguerreiro.resilient.repository.security.SecurityResourceRepository; +import com.oguerreiro.resilient.security.resillient.SecurityResource; +import com.oguerreiro.resilient.service.AbstractResilientService; +import com.oguerreiro.resilient.service.dto.security.SecurityResourceDTO; +import com.oguerreiro.resilient.service.mapper.security.SecurityResourceMapper; + +/** + * Service Implementation for managing {@link com.oguerreiro.resilient.security.SecurityResource}. + */ +@Service +@Transactional +public class SecurityResourceService extends AbstractResilientService { + + public SecurityResourceService(SecurityResourceRepository securityResourceRepository, + SecurityResourceMapper securityResourceMapper) { + super(SecurityResource.class, securityResourceRepository, securityResourceMapper); + + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/web/filter/SpaWebFilter.java b/src/main/java/com/oguerreiro/resilient/web/filter/SpaWebFilter.java new file mode 100644 index 0000000..8841085 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/filter/SpaWebFilter.java @@ -0,0 +1,41 @@ +package com.oguerreiro.resilient.web.filter; + +import java.io.IOException; + +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class SpaWebFilter extends OncePerRequestFilter { + + /** + * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}. + */ + //@formatter:off + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // Request URI includes the contextPath if any, removed it. + String path = request.getRequestURI().substring(request.getContextPath().length()); + if ( + !path.startsWith("/saml2") && + !path.startsWith("/login/saml2") && + !path.startsWith("/logout/saml2") && + !path.startsWith("/api") && + !path.startsWith("/management") && + !path.startsWith("/v3/api-docs") && + !path.contains(".") && + path.matches("/(.*)") + ) { + request.getRequestDispatcher("/index.html").forward(request, response); + return; + } + + filterChain.doFilter(request, response); + } + //@formatter:on +} diff --git a/src/main/java/com/oguerreiro/resilient/web/filter/package-info.java b/src/main/java/com/oguerreiro/resilient/web/filter/package-info.java new file mode 100644 index 0000000..2c85035 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/filter/package-info.java @@ -0,0 +1,4 @@ +/** + * Request chain filters. + */ +package com.oguerreiro.resilient.web.filter; diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/AbstractResilientQueryResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/AbstractResilientQueryResource.java new file mode 100644 index 0000000..4aaa8cf --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/AbstractResilientQueryResource.java @@ -0,0 +1,42 @@ +package com.oguerreiro.resilient.web.rest; + +import java.io.Serializable; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.repository.ResilientJpaRepository; +import com.oguerreiro.resilient.service.AbstractResilientService; +import com.oguerreiro.resilient.service.criteria.AbstractCriteria; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; +import com.oguerreiro.resilient.service.query.AbstractQueryService; + +public abstract class AbstractResilientQueryResource, DTO extends AbstractResilientDTO, ID extends Serializable, C extends AbstractCriteria> + extends AbstractResilientResource { + + private final AbstractQueryService domainQueryService; + + protected AbstractResilientQueryResource(Class domainClass, ResilientJpaRepository domainRepository, + AbstractResilientService domainService, AbstractQueryService domainQueryService) { + super(domainClass, domainRepository, domainService); + this.domainQueryService = domainQueryService; + } + + /** + * {@code GET //criteria} : get with a criteria. + * + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of entity domains in body. + */ + @PreAuthorize("@resilientSecurity.canAccess()") + @GetMapping("/criteria") + public ResponseEntity> getAllByCriteria(C criteria) { + log.debug("REST request to getAllByCriteria by criteria: {}", criteria); + + List domainList = domainQueryService.findByCriteria(criteria, this.getAllSort()); + return ResponseEntity.ok().body(domainList); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/AbstractResilientResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/AbstractResilientResource.java new file mode 100644 index 0000000..0ff57ef --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/AbstractResilientResource.java @@ -0,0 +1,335 @@ +package com.oguerreiro.resilient.web.rest; + +import java.io.Serializable; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.oguerreiro.resilient.domain.AbstractResilientEntity; +import com.oguerreiro.resilient.domain.DashboardComponent; +import com.oguerreiro.resilient.repository.ResilientJpaRepository; +import com.oguerreiro.resilient.service.AbstractResilientService; +import com.oguerreiro.resilient.service.dto.AbstractResilientDTO; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * Resilient abstract base resource.
+ * All REST endpoints should extend this {@link AbstractResilientResource}. Implementation looks like: + * + *
+ * @RestController
+ * @RequestMapping("/api/my-sample")
+ * public class MySampleResource extends AbstractResource {
+ *   private static final String ENTITY_NAME = "mySample";
+ * 
+ *   public MySampleResource(MySampleRepository mySampleRepository, MySampleService mySampleService) {
+ *     super(MySample.class, mySampleRepository, mySampleService);
+ *   }
+ * 
+ *   @Override
+ *   protected String getEntityName() {
+ *     return ENTITY_NAME;
+ *   }
+ * }
+ * 
+ * + * The resource implementation class, will define the base URI mapping ({@code @RequestMapping("/api/my-sample")}).
+ * The {@link AbstractResilientResource } provides REST basic operations (CRUD): + *

REST Base Endpoints

+ *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
URIVERBDescription
GETList endpoint. See {@link AbstractResilientResource#getAll()}.
POSTCreate endpoint. See {@link AbstractResilientResource#create(AbstractResilientDTO)}.
/{id}PUTUpdate endpoint. See {@link AbstractResilientResource#update(Serializable, AbstractResilientDTO)}.
/{id}PATCHPartial Update endpoint. See + * {@link AbstractResilientResource#partialUpdate(Serializable, AbstractResilientDTO)}.
/{id}GETRead endpoint. See {@link AbstractResilientResource#getEntity(Serializable)}.
/{id}DELETEDelete endpoint. See {@link AbstractResilientResource#delete(Serializable)}.
+ *

+ * + * @param the mapped entity domain class type. Example: MySample + * @param the entity domain DTO class, for . Example: MySampleDTO + * @param the entity domain key class type. Example: Long, assuming the id is a Long + */ +public abstract class AbstractResilientResource, DTO extends AbstractResilientDTO, ID extends Serializable> { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + // Constants to enforce public knowledge of Abstract endpoint method names. + public static final String ENDPOINT_METHOD_NAME_CREATE = "create"; + public static final String ENDPOINT_METHOD_NAME_READ = "getEntity"; + public static final String ENDPOINT_METHOD_NAME_UPDATE = "update"; + public static final String ENDPOINT_METHOD_NAME_PARTIAL = "partialUpdate"; + public static final String ENDPOINT_METHOD_NAME_DELETE = "delete"; + public static final String ENDPOINT_METHOD_NAME_LIST = "getAll"; + public static final String ENDPOINT_METHOD_NAME_CRITERIA_LIST = "getAllByCriteria"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private String domainSimpleName; + private String requestMappingDefaultURI; + + private final AbstractResilientService domainService; + private final ResilientJpaRepository domainRepository; + private final Class domainClass; // This is a best practice to get the domain class. To infer from the generic (DOMAIN) is VERY hard. + + protected AbstractResilientResource(Class domainClass, ResilientJpaRepository domainRepository, + AbstractResilientService domainService) { + this.domainClass = domainClass; + this.domainRepository = domainRepository; + this.domainService = domainService; + + // Set some sugar values + this.domainSimpleName = this.domainClass.getSimpleName(); //Used in logging + } + + protected final AbstractResilientService getService() { + return this.domainService; + } + + /* *** Abstract methods *** */ + /* ********************************************************************************* */ + /** + * Provide a functional name for the entity. Usually the domain class name, cammel cased, starting with lowercase. + * Example, for domain the {@link DashboardComponent} should return 'dashboardComponent'. This is a suggestion only. + * + * @return a String with the functional entity name + */ + protected abstract String getEntityName(); + + /* *** Public methods (server side) *** */ + /* ********************************************************************************* */ + /** + * The domain which this Resource provides endpoints for. DON'T be confused with getClass() that will return the Class + * of {@code this} instance. + * + * @return + */ + public Class getDomainClass() { + return this.domainClass; + } + + /* *** STANDARD REST endpoints *** */ + /* ********************************************************************************* */ + /** + * {@code POST /} : Create a new entity. + * + * @param domainDTO the domainDTO to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new + * domainDTO, or with status {@code 400 (Bad Request)} if the domain has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PreAuthorize("@resilientSecurity.canAccess()") + @PostMapping("") + public ResponseEntity create(@Valid @RequestBody DTO domainDTO) throws URISyntaxException { + log.debug("REST request to save " + this.domainSimpleName + " : {}", domainDTO); + if (domainDTO.getId() != null) { + throw new BadRequestAlertException("A new " + this.domainSimpleName + " cannot already have an ID", + this.getEntityName(), "idexists"); + } + domainDTO = domainService.save(domainDTO); + return ResponseEntity.created(new URI(getRequestMappingDefaultURI() + domainDTO.getId())).headers( + HeaderUtil.createEntityCreationAlert(applicationName, true, this.getEntityName(), + domainDTO.getId().toString())).body(domainDTO); + } + + /** + * {@code PUT //:id} : Updates an existing entity. + * + * @param id the id of the domainDTO to save. + * @param domainDTO the domainDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated + * domainDTO, + * or with status {@code 400 (Bad Request)} if the domainDTO is not valid, + * or with status {@code 500 (Internal Server Error)} if the domainDTO couldn't be + * updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PreAuthorize("@resilientSecurity.canAccess()") + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable(value = "id", required = false) final ID id, + @Valid @RequestBody DTO domainDTO) throws URISyntaxException { + log.debug("REST request to update " + this.domainSimpleName + " : {}, {}", id, domainDTO); + if (domainDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", getEntityName(), "idnull"); + } + if (!Objects.equals(id, domainDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", getEntityName(), "idinvalid"); + } + + if (!domainRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", getEntityName(), "idnotfound"); + } + + domainDTO = domainService.update(domainDTO); + return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, this.getEntityName(), + domainDTO.getId().toString())).body(domainDTO); + } + + /** + * {@code PATCH //:id} : Partial updates given fields of an existing entity, field will ignore if it is + * null + * + * @param id the id of the domainDTO to save. + * @param domainDTO the domainDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated + * domainDTO, + * or with status {@code 400 (Bad Request)} if the domainDTO is not valid, + * or with status {@code 404 (Not Found)} if the domainDTO is not found, + * or with status {@code 500 (Internal Server Error)} if the domainDTO couldn't be + * updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PreAuthorize("@resilientSecurity.canAccess()") + @PatchMapping(value = "/{id}", consumes = { "application/json", "application/merge-patch+json" }) + public ResponseEntity partialUpdate(@PathVariable(value = "id", required = false) final ID id, + @NotNull @RequestBody DTO domainDTO) throws URISyntaxException { + log.debug("REST request to partial update " + this.domainSimpleName + " partially : {}, {}", id, domainDTO); + if (domainDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", getEntityName(), "idnull"); + } + if (!Objects.equals(id, domainDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", getEntityName(), "idinvalid"); + } + + if (!domainRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", getEntityName(), "idnotfound"); + } + + Optional result = domainService.partialUpdate(domainDTO); + + return ResponseUtil.wrapOrNotFound(result, + HeaderUtil.createEntityUpdateAlert(applicationName, true, getEntityName(), domainDTO.getId().toString())); + } + + /** + * {@code GET /} : get all the entity. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of entities in body. + */ + @PreAuthorize("@resilientSecurity.canAccess()") + @GetMapping("") + public List getAll() { + log.debug("REST request to get all " + this.domainSimpleName + "'s."); + return domainService.findAll(this.getAllSort()); + } + + /** + * Override this to provide a ORDER BY for the getAll endpoint. + * Example: return Sort.by(Sort.Order.asc("name"), Sort.Order.desc("id")); + * + * @return + */ + protected Sort getAllSort() { + return Sort.unsorted(); + } + + /** + * {@code GET //:id} : get entity by the "id". + * + * @param id the id of the domainDTO to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the domainDTO, or with status + * {@code 404 (Not Found)}. + */ + @PreAuthorize("@resilientSecurity.canAccess()") + @GetMapping("/{id}") + public ResponseEntity getEntity(@PathVariable("id") ID id) { + log.debug("REST request to get " + this.domainSimpleName + " : {}", id); + Optional domainDTO = domainService.findOne(id); + return ResponseUtil.wrapOrNotFound(domainDTO); + } + + /** + * {@code DELETE //:id} : delete entity by the "id". + * + * @param id the id of the domainDTO to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @PreAuthorize("@resilientSecurity.canAccess()") + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable("id") ID id) { + log.debug("REST request to delete " + this.domainSimpleName + " : {}", id); + domainService.delete(id); + return ResponseEntity.noContent().headers( + HeaderUtil.createEntityDeletionAlert(applicationName, true, getEntityName(), id.toString())).build(); + } + + /** + * Dynamically extracts the endpoint URI from the @RequestMapping, needed for responses, + *

+ * For the next example, it will return : "/api/dashboard-components". + * + *

+   * 
+   * @RestController
+   * @RequestMapping("/api/dashboard-components")
+   * public class DashboardComponentResource extends AbstractResource {
+   * 
+   * 
+ *

+ */ + private String getRequestMappingDefaultURI() { + if (this.requestMappingDefaultURI == null) { + RequestMapping requestMapping = this.getClass().getAnnotation(RequestMapping.class); + if (requestMapping == null || requestMapping.path() == null || requestMapping.path().length == 0) { + this.requestMappingDefaultURI = ""; + } else { + this.requestMappingDefaultURI = requestMapping.path()[0]; + } + } + + return this.requestMappingDefaultURI; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/AccountResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/AccountResource.java new file mode 100644 index 0000000..d4eb475 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/AccountResource.java @@ -0,0 +1,316 @@ +package com.oguerreiro.resilient.web.rest; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.config.SecurityConfiguration; +import com.oguerreiro.resilient.domain.PersistentToken; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.PersistentTokenRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.security.custom.ResilientUserDetails; +import com.oguerreiro.resilient.service.MailService; +import com.oguerreiro.resilient.service.UserService; +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import com.oguerreiro.resilient.service.dto.PasswordChangeDTO; +import com.oguerreiro.resilient.web.rest.errors.EmailAlreadyUsedException; +import com.oguerreiro.resilient.web.rest.errors.InvalidPasswordException; +import com.oguerreiro.resilient.web.rest.errors.LoginAlreadyUsedException; +import com.oguerreiro.resilient.web.rest.vm.KeyAndPasswordVM; +import com.oguerreiro.resilient.web.rest.vm.ManagedUserVM; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; + +/** + * REST controller for managing the current user's account. + */ +@RestController +@RequestMapping("/api") +public class AccountResource { + + private static class AccountResourceException extends RuntimeException { + + private AccountResourceException(String message) { + super(message); + } + } + + private final Logger log = LoggerFactory.getLogger(AccountResource.class); + + private final UserRepository userRepository; + + private final UserService userService; + + private final MailService mailService; + + private final PersistentTokenRepository persistentTokenRepository; + + private final RelyingPartyRegistrationRepository registrationRepository; + + public AccountResource(UserRepository userRepository, UserService userService, MailService mailService, + PersistentTokenRepository persistentTokenRepository, + @Autowired(required = false) RelyingPartyRegistrationRepository registrationRepository) { + this.userRepository = userRepository; + this.userService = userService; + this.mailService = mailService; + this.persistentTokenRepository = persistentTokenRepository; + this.registrationRepository = registrationRepository; + } + + /** + * {@code POST /register} : register the user. + * + * @param managedUserVM the managed user View Model. + * @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect. + * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used. + * @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already used. + */ + @PostMapping("/register") + @ResponseStatus(HttpStatus.CREATED) + public void registerAccount(@Valid @RequestBody ManagedUserVM managedUserVM) { + if (isPasswordLengthInvalid(managedUserVM.getPassword())) { + throw new InvalidPasswordException(); + } + User user = userService.registerUser(managedUserVM, managedUserVM.getPassword()); + mailService.sendActivationEmail(user); + } + + /** + * {@code GET /activate} : activate the registered user. + * + * @param key the activation key. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the user couldn't be activated. + */ + @GetMapping("/activate") + public void activateAccount(@RequestParam(value = "key") String key) { + Optional user = userService.activateRegistration(key); + if (!user.isPresent()) { + throw new AccountResourceException("No user was found for this activation key"); + } + } + + /** + * {@code GET /authenticate} : check if the user is authenticated, and return its login. + * + * @param request the HTTP request. + * @return the login if the user is authenticated. + */ + @GetMapping("/authenticate") + public String isAuthenticated(HttpServletRequest request) { + log.debug("REST request to check if the current user is authenticated"); + return request.getRemoteUser(); + } + + /** + * {@code GET /account} : get the current user. + * + * @return the current user. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the user couldn't be returned. + */ + @GetMapping("/account") + public AdminUserDTO getAccount() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + AdminUserDTO adminUserDTO = null; + ResilientUserDetails resilientUserDetails = null; + + // Distinguish the type of principal + if (principal instanceof Saml2AuthenticatedPrincipal) { + // This is a SAMLv2 auth + Saml2AuthenticatedPrincipal samlPrincipal = (Saml2AuthenticatedPrincipal) principal; + adminUserDTO = new AdminUserDTO(); + adminUserDTO.setLogin(samlPrincipal.getName()); + + } else if (principal instanceof ResilientUserDetails) { + // This is a form + resilientUserDetails = (ResilientUserDetails) principal; + adminUserDTO = Optional.of(resilientUserDetails).map(AdminUserDTO::new).orElseThrow( + () -> new AccountResourceException("User could not be found")); + } + + return adminUserDTO; + + /* + return userService + .getUserWithAuthorities() + .map(AdminUserDTO::new) + .orElseThrow(() -> new AccountResourceException("User could not be found")); + */ + } + + /** + * {@code POST /account} : update the current user information. + * + * @param userDTO the current user information. + * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the user login wasn't found. + */ + @PostMapping("/account") + public void saveAccount(@Valid @RequestBody AdminUserDTO userDTO) { + String userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow( + () -> new AccountResourceException("Current user login not found")); + Optional existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.orElseThrow().getLogin().equalsIgnoreCase(userLogin))) { + throw new EmailAlreadyUsedException(); + } + Optional user = userRepository.findOneByLogin(userLogin); + if (!user.isPresent()) { + throw new AccountResourceException("User could not be found"); + } + userService.updateUser(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(), userDTO.getLangKey(), + userDTO.getImageUrl()); + } + + /** + * {@code POST /account/change-password} : changes the current user's password. + * + * @param passwordChangeDto current and new password. + * @throws InvalidPasswordException {@code 400 (Bad Request)} if the new password is incorrect. + */ + @PostMapping(path = "/account/change-password") + public void changePassword(@RequestBody PasswordChangeDTO passwordChangeDto) { + if (isPasswordLengthInvalid(passwordChangeDto.getNewPassword())) { + throw new InvalidPasswordException(); + } + userService.changePassword(passwordChangeDto.getCurrentPassword(), passwordChangeDto.getNewPassword()); + } + + /** + * {@code GET /account/sessions} : get the current open sessions. + * + * @return the current open sessions. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the current open sessions couldn't be retrieved. + */ + @GetMapping("/account/sessions") + public List getCurrentSessions() { + return persistentTokenRepository.findByUser( + userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin().orElseThrow( + () -> new AccountResourceException("Current user login not found"))).orElseThrow( + () -> new AccountResourceException("User could not be found"))); + } + + /** + * {@code DELETE /account/sessions?series={series}} : invalidate an existing session. + * + * - You can only delete your own sessions, not any other user's session + * - If you delete one of your existing sessions, and that you are currently logged in on that session, you will + * still be able to use that session, until you quit your browser: it does not work in real time (there is + * no API for that), it only removes the "remember me" cookie + * - This is also true if you invalidate your current session: you will still be able to use it until you close + * your browser or that the session times out. But automatic login (the "remember me" cookie) will not work + * anymore. + * There is an API to invalidate the current session, but there is no API to check which session uses which + * cookie. + * + * @param series the series of an existing session. + * @throws IllegalArgumentException if the series couldn't be URL decoded. + */ + @DeleteMapping("/account/sessions/{series}") + public void invalidateSession(@PathVariable("series") String series) { + String decodedSeries = URLDecoder.decode(series, StandardCharsets.UTF_8); + SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneByLogin).ifPresent( + u -> persistentTokenRepository.findByUser(u).stream().filter( + persistentToken -> StringUtils.equals(persistentToken.getSeries(), decodedSeries)).findAny().ifPresent( + t -> persistentTokenRepository.deleteById(decodedSeries))); + } + + /** + * {@code POST /account/reset-password/init} : Send an email to reset the password of the user. + * + * @param mail the mail of the user. + */ + @PostMapping(path = "/account/reset-password/init") + public void requestPasswordReset(@RequestBody String mail) { + Optional user = userService.requestPasswordReset(mail); + if (user.isPresent()) { + mailService.sendPasswordResetMail(user.orElseThrow()); + } else { + // Pretend the request has been successful to prevent checking which emails really exist + // but log that an invalid attempt has been made + log.warn("Password reset requested for non existing mail"); + } + } + + /** + * {@code POST /account/reset-password/finish} : Finish to reset the password of the user. + * + * @param keyAndPassword the generated key and the new password. + * @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the password could not be reset. + */ + @PostMapping(path = "/account/reset-password/finish") + public void finishPasswordReset(@RequestBody KeyAndPasswordVM keyAndPassword) { + if (isPasswordLengthInvalid(keyAndPassword.getNewPassword())) { + throw new InvalidPasswordException(); + } + Optional user = userService.completePasswordReset(keyAndPassword.getNewPassword(), keyAndPassword.getKey()); + + if (!user.isPresent()) { + throw new AccountResourceException("No user was found for this reset key"); + } + } + + /** + * Returns the SAMLv2 endpoint configured, if any. + * Config this for anonymous access, in + * {@link SecurityConfiguration#filterChain(org.springframework.security.config.annotation.web.builders.HttpSecurity, org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher.Builder, com.oguerreiro.resilient.security.saml2.Saml2AuthenticationSuccessHandler) + * SecurityConfiguration.filterChain()}: + * + *
+   * .requestMatchers(mvc.pattern("/api/account/saml2-endpoint")).permitAll()
+   * 
+ * + * @return + */ + @GetMapping(path = "/account/saml2-endpoint") + public String saml2Endpoint() { + List ids = this.getRegistrationIds(); + if (ids == null || ids.isEmpty()) { + return null; + } + return ids.get(0); + } + + private static boolean isPasswordLengthInvalid(String password) { + return (StringUtils.isEmpty(password) || password.length() < ManagedUserVM.PASSWORD_MIN_LENGTH + || password.length() > ManagedUserVM.PASSWORD_MAX_LENGTH); + } + + public List getRegistrationIds() { + if (registrationRepository == null) + return null; + + List ids = new ArrayList<>(); + if (registrationRepository instanceof Iterable iterable) { + for (Object item : iterable) { + if (item instanceof RelyingPartyRegistration reg) { + ids.add(reg.getRegistrationId()); + } + } + } + return ids; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/ActivityEngineResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/ActivityEngineResource.java new file mode 100644 index 0000000..7afc7f5 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/ActivityEngineResource.java @@ -0,0 +1,115 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.activity.Activity; +import com.oguerreiro.resilient.activity.ActivityRegistry; +import com.oguerreiro.resilient.activity.ActivityResult; +import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor; +import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry; +import com.oguerreiro.resilient.service.dto.activity.ActivityInfoDTO; +import com.oguerreiro.resilient.service.dto.activity.ActivityInvokeDTO; +import com.oguerreiro.resilient.service.dto.activity.ActivityInvokeResponseDTO; +import com.oguerreiro.resilient.service.dto.activity.ActivityProgressDTO; +import com.oguerreiro.resilient.service.mapper.ActivityProgressMapper; + +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Organization}. + */ +@RestController +@RequestMapping("/api/activity-engines") +public class ActivityEngineResource { + + private final Logger log = LoggerFactory.getLogger(ActivityEngineResource.class); + + private static final String ENTITY_NAME = "activity-engines"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final ActivityRegistry activityRegistry; + private final ActivityProgressRegistry activityProgressRegistry; + private final ActivityProgressMapper activityProgressMapper; + + public ActivityEngineResource(ActivityProgressRegistry activityProgressRegistry, ActivityProgressMapper activityProgressMapper, ActivityRegistry activityRegistry) { + this.activityProgressRegistry = activityProgressRegistry; + this.activityProgressMapper = activityProgressMapper; + this.activityRegistry = activityRegistry; + } + + /** + * {@code GET /activity-engines/progress/:activityRegistryKey} + * + * @param activityRegistryKey the key of the Entity to search for within the progress registry. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the activityProgressDTO, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/progress/{activityRegistryKey}") + public ResponseEntity getProgress( + @PathVariable("activityRegistryKey") String activityRegistryKey) + { + log.debug("REST request to get Activity progress : {}", activityRegistryKey); + + ActivityProgressDescriptor descriptor = activityProgressRegistry.progressOf(activityRegistryKey); + + ActivityProgressDTO dto = activityProgressMapper.toDto(descriptor); + if (dto == null) { + // Always return a progress + dto = new ActivityProgressDTO(); + dto.setProgressPercentage(-1); // Means that at this moment nothing is in progress for this Entity domain + } + return ResponseUtil.wrapOrNotFound(Optional.of(dto)); + } + + @GetMapping("/activity/{class}/{state}") + public ResponseEntity> getActivity( + @PathVariable("class") String activityDomainClass, + @PathVariable("state") String activityDomainState) + { + log.debug("REST request to get Activity info : {}", activityDomainClass, activityDomainState); + + Map activities = activityRegistry.findActivitiesFor(activityDomainClass, activityDomainState); + List response = new ArrayList(); + for (String uuid : activities.keySet()) { + response.add(new ActivityInfoDTO(uuid)); + } + + return ResponseEntity.ok().body(response); + } + + @PutMapping("/activity") + public ResponseEntity doActivity( + @RequestBody ActivityInvokeDTO activityInvokeDTO) + { + log.debug("REST request to get doActivity : {}", activityInvokeDTO); + + //Get the activity by UUID + Activity activity = activityRegistry.getActivity(activityInvokeDTO.getActivityKey(), activityInvokeDTO.getActivityDomainClass()); + + //Execute activity + ActivityResult ar = activity.doActivity(activityInvokeDTO.getActivityDomainClass(), activityInvokeDTO.getActivityDomainId()); + + ActivityInvokeResponseDTO responseDto = new ActivityInvokeResponseDTO(); + responseDto.setActivityUUID(activityInvokeDTO.getActivityKey()); + + return ResponseEntity.ok() + .headers(HeaderUtil.createAlert(applicationName, "Activity has started ...", activityInvokeDTO.getActivityDomainClass())) + .body(responseDto); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/AuthorityResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/AuthorityResource.java new file mode 100644 index 0000000..6688b91 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/AuthorityResource.java @@ -0,0 +1,101 @@ +package com.oguerreiro.resilient.web.rest; + +import com.oguerreiro.resilient.domain.Authority; +import com.oguerreiro.resilient.repository.AuthorityRepository; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; +import jakarta.validation.Valid; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Authority}. + */ +@RestController +@RequestMapping("/api/authorities") +@Transactional +public class AuthorityResource { + + private final Logger log = LoggerFactory.getLogger(AuthorityResource.class); + + private static final String ENTITY_NAME = "adminAuthority"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final AuthorityRepository authorityRepository; + + public AuthorityResource(AuthorityRepository authorityRepository) { + this.authorityRepository = authorityRepository; + } + + /** + * {@code POST /authorities} : Create a new authority. + * + * @param authority the authority to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new authority, or with status {@code 400 (Bad Request)} if the authority has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity createAuthority(@Valid @RequestBody Authority authority) throws URISyntaxException { + log.debug("REST request to save Authority : {}", authority); + if (authorityRepository.existsById(authority.getName())) { + throw new BadRequestAlertException("authority already exists", ENTITY_NAME, "idexists"); + } + authority = authorityRepository.save(authority); + return ResponseEntity.created(new URI("/api/authorities/" + authority.getName())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, authority.getName())) + .body(authority); + } + + /** + * {@code GET /authorities} : get all the authorities. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of authorities in body. + */ + @GetMapping("") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public List getAllAuthorities() { + log.debug("REST request to get all Authorities"); + return authorityRepository.findAll(); + } + + /** + * {@code GET /authorities/:id} : get the "id" authority. + * + * @param id the id of the authority to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the authority, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity getAuthority(@PathVariable("id") String id) { + log.debug("REST request to get Authority : {}", id); + Optional authority = authorityRepository.findById(id); + return ResponseUtil.wrapOrNotFound(authority); + } + + /** + * {@code DELETE /authorities/:id} : delete the "id" authority. + * + * @param id the id of the authority to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity deleteAuthority(@PathVariable("id") String id) { + log.debug("REST request to delete Authority : {}", id); + authorityRepository.deleteById(id); + return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id)).build(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/ContentPageResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/ContentPageResource.java new file mode 100644 index 0000000..200378a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/ContentPageResource.java @@ -0,0 +1,64 @@ +package com.oguerreiro.resilient.web.rest; + +import java.net.URISyntaxException; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.ContentPage; +import com.oguerreiro.resilient.repository.ContentPageRepository; +import com.oguerreiro.resilient.service.ContentPageService; +import com.oguerreiro.resilient.service.dto.ContentPageDTO; + +import jakarta.validation.Valid; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.ContentPage}. + */ +@RestController +@RequestMapping("/api/content-pages") +public class ContentPageResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "contentPage"; + + public ContentPageResource(ContentPageRepository contentPageRepository, ContentPageService contentPageService) { + super(ContentPage.class, contentPageRepository, contentPageService); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* Override endpoints */ + /** + * {@code DELETE /content-pages/:id} : delete entity by the "id". + * UNSUPPORTED. CANT DELETE CONTENTPAGE. + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @DeleteMapping("/{id}") + public ResponseEntity delete(Long id) { + log.debug("REST request to delete : {}", id); + throw new UnsupportedOperationException(); + } + + /** + * {@code POST /content-pages} : Create a new entity. + * UNSUPPORTED. CANT CREATE NEW CONTENTPAGE. + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @PostMapping("") + public ResponseEntity create(@Valid ContentPageDTO domainDTO) throws URISyntaxException { + log.debug("REST request to create : {}", domainDTO); + throw new UnsupportedOperationException(); + } + + /* DashboardResource endpoints */ +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/DashboardComponentResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/DashboardComponentResource.java new file mode 100644 index 0000000..1724b5b --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/DashboardComponentResource.java @@ -0,0 +1,119 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.DashboardComponent; +import com.oguerreiro.resilient.domain.DashboardComponentDetailValue; +import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView; +import com.oguerreiro.resilient.repository.DashboardComponentRepository; +import com.oguerreiro.resilient.service.DashboardComponentService; +import com.oguerreiro.resilient.service.dto.DashboardComponentDTO; +import com.oguerreiro.resilient.service.dto.DashboardComponentDetailValueDTO; +import com.oguerreiro.resilient.service.mapper.DashboardComponentDetailValueMapper; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.UnitType}. + */ +@RestController +@RequestMapping("/api/dashboard-components") +public class DashboardComponentResource + extends AbstractResilientResource { + + private static final String ENTITY_NAME = "dashboardComponent"; + private final DashboardComponentDetailValueMapper dashboardComponentDetailValueMapper; + private final DashboardComponentService dashboardComponentService; + + public DashboardComponentResource(DashboardComponentRepository dashboardComponentRepository, + DashboardComponentService dashboardComponentService, + DashboardComponentDetailValueMapper dashboardComponentDetailValueMapper) { + super(DashboardComponent.class, dashboardComponentRepository, dashboardComponentService); + this.dashboardComponentDetailValueMapper = dashboardComponentDetailValueMapper; + this.dashboardComponentService = dashboardComponentService; + } + + private DashboardComponentService getDashboardComponentService() { + return (DashboardComponentService) super.getService(); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* DashboardComponentResource endpoints */ + /** + * {@code GET //build/{id}} : builds a component filled with Output calculated values, ready to show + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of entities in body. + */ + @GetMapping("/build/{dashboardComponentId}/{periodVersionId}") + public Map> buildComponent( + @PathVariable("dashboardComponentId") Long dashboardComponentId, + @PathVariable("periodVersionId") Long periodVersionId) { + log.debug("REST request to get DashboardComponentView's."); + + Optional dashboardComponentDTO = getDashboardComponentService().findOne( + dashboardComponentId); + Map> values = getDashboardComponentService().buildDashboardComponentView( + null, dashboardComponentId, periodVersionId); + + return this.dashboardComponentDetailValueMapper.mapToDto(values); + } + + @GetMapping("/build/{organizationId}/{dashboardComponentId}/{periodVersionId}") + public Map> buildOrganizationComponent( + @PathVariable("organizationId") Long organizationId, + @PathVariable("dashboardComponentId") Long dashboardComponentId, + @PathVariable("periodVersionId") Long periodVersionId) { + log.debug("REST request to get DashboardComponentView's."); + + Optional dashboardComponentDTO = getDashboardComponentService().findOne( + dashboardComponentId); + Map> values = getDashboardComponentService().buildDashboardComponentView( + organizationId, dashboardComponentId, periodVersionId); + + return this.dashboardComponentDetailValueMapper.mapToDto(values); + } + + @GetMapping("/active") + public List getAllActive() { + log.debug("REST request to get all DashboardComponent's."); + return getDashboardComponentService().findAllActive(); + } + + @GetMapping("/active/{view}") + public List getAllActiveForView(@PathVariable("view") String dashboardComponentViewString) { + log.debug("REST request to getAllActiveForView."); + + DashboardComponentView dashboardComponentView = DashboardComponentView.valueOf(dashboardComponentViewString); + return getDashboardComponentService().findAllActiveForView(dashboardComponentView); + } + + @GetMapping("/export/{organizationId}/{dashboardComponentId}/{periodVersionId}") + public ResponseEntity export(@PathVariable("organizationId") Long organizationId, + @PathVariable("dashboardComponentId") Long dashboardComponentId, + @PathVariable("periodVersionId") Long periodVersionId) { + log.debug("REST request to export : {}", organizationId, dashboardComponentId, periodVersionId); + + byte[] fileContent = this.dashboardComponentService.export(organizationId, dashboardComponentId, periodVersionId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDisposition(ContentDisposition.attachment().filename("report.xlsx").build()); + + return new ResponseEntity<>(fileContent, headers, HttpStatus.OK); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/DashboardResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/DashboardResource.java new file mode 100644 index 0000000..c56d880 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/DashboardResource.java @@ -0,0 +1,41 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.Dashboard; +import com.oguerreiro.resilient.repository.DashboardRepository; +import com.oguerreiro.resilient.service.DashboardService; +import com.oguerreiro.resilient.service.dto.DashboardDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Dashboard}. + */ +@RestController +@RequestMapping("/api/dashboards") +public class DashboardResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "dashboard"; + private final DashboardService dashboardService; + + public DashboardResource(DashboardRepository dashboardRepository, DashboardService dashboardService) { + super(Dashboard.class, dashboardRepository, dashboardService); + this.dashboardService = dashboardService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* DashboardResource endpoints */ + @GetMapping("/internal") + public List getAllInternal() { + log.debug("REST request to get all Dashboard's."); + return this.dashboardService.findAllInternal(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/DocumentResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/DocumentResource.java new file mode 100644 index 0000000..f52c8a9 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/DocumentResource.java @@ -0,0 +1,33 @@ +package com.oguerreiro.resilient.web.rest; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.Document; +import com.oguerreiro.resilient.repository.DocumentRepository; +import com.oguerreiro.resilient.service.DocumentService; +import com.oguerreiro.resilient.service.dto.DocumentDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Document}. + */ +@RestController +@RequestMapping("/api/documents") +public class DocumentResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "document"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + public DocumentResource(DocumentService documentService, DocumentRepository documentRepository) { + super(Document.class, documentRepository, documentService); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/EmissionFactorResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/EmissionFactorResource.java new file mode 100644 index 0000000..ea97440 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/EmissionFactorResource.java @@ -0,0 +1,84 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.EmissionFactor; +import com.oguerreiro.resilient.repository.EmissionFactorRepository; +import com.oguerreiro.resilient.service.criteria.EmissionFactorCriteria; +import com.oguerreiro.resilient.service.dto.EmissionFactorDTO; +import com.oguerreiro.resilient.service.query.EmissionFactorQueryService; +import com.oguerreiro.resilient.service.query.EmissionFactorService; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.EmissionFactor}. + */ +@RestController +@RequestMapping("/api/emission-factors") +public class EmissionFactorResource + extends AbstractResilientQueryResource { + + private static final String ENTITY_NAME = "emissionFactor"; + + private final EmissionFactorService emissionFactorService; + + public EmissionFactorResource(EmissionFactorService emissionFactorService, + EmissionFactorRepository emissionFactorRepository, EmissionFactorQueryService emissionFactorQueryService) { + super(EmissionFactor.class, emissionFactorRepository, emissionFactorService, emissionFactorQueryService); + this.emissionFactorService = emissionFactorService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* EmissionFactorResource endpoints */ + @GetMapping("/export") + public ResponseEntity export(EmissionFactorCriteria criteria) { + log.debug("REST request to export : {}"); + + Integer year = criteria.getYear() == null ? null : criteria.getYear().getEquals(); + Long scopeId = criteria.getVariableScopeId() == null ? null : criteria.getVariableScopeId().getEquals(); + Long categoryId = criteria.getVariableCategoryId() == null ? null : criteria.getVariableCategoryId().getEquals(); + + byte[] fileContent = this.emissionFactorService.export(year, scopeId, categoryId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDisposition(ContentDisposition.attachment().filename("report.xlsx").build()); + + return new ResponseEntity<>(fileContent, headers, HttpStatus.OK); + } + + @GetMapping("/search/{year}/{scopeId}/{categoryId}") + public ResponseEntity> getAllBySearch( + @PathVariable(value = "year", required = true) Integer year, + @PathVariable(value = "scopeId", required = false) String scopeId, + @PathVariable(value = "categoryId", required = false) String categoryId) { + log.debug("REST request to getAllBySearch by: {}", year, scopeId, categoryId); + + Long searchScopeId = null; + if (scopeId != null && !scopeId.equals("ALL")) { + searchScopeId = Long.valueOf(scopeId); + } + + Long searchCategoryId = null; + if (categoryId != null && !categoryId.equals("ALL")) { + searchCategoryId = Long.valueOf(categoryId); + } + + List domainList = emissionFactorService.findAllBySearch(year, searchScopeId, searchCategoryId); + return ResponseEntity.ok().body(domainList); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/EnvironmentResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/EnvironmentResource.java new file mode 100644 index 0000000..380aa58 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/EnvironmentResource.java @@ -0,0 +1,68 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.service.OrganizationService; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; + +/** + * REST controller for environment request + */ +@RestController +@RequestMapping("/api/env") +public class EnvironmentResource { + + private final Logger log = LoggerFactory.getLogger(EnvironmentResource.class); + + private static final String ENTITY_NAME = "environment"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final OrganizationService organizationService; + + public EnvironmentResource(OrganizationService organizationService) { + this.organizationService = organizationService; + } + + /** + * {@code GET /organizations} : get the organizations for the current user. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of organizations in body. + */ + @PreAuthorize("@resilientSecurity.canAccess('READ', 'com.oguerreiro.resilient.domain.Organization')") // Inventory, delegates permission to Organization + @GetMapping("/organizations") + public List getAllOrganizations( + @RequestParam(name = "eagerload", required = false, defaultValue = "true") boolean eagerload) { + log.debug("REST request to get all Organizations"); + + /* + THE CUSTOMER CHANGED THE RULE. ALL Users will see ALL Organizations. + + //Get current authenticated user + User currentUser = SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneByLogin).orElseThrow(); + + //Search the PERSON for this user + Organization person = organizationRepository.findOneByUser(currentUser).orElse(null); + if (person == null) { + return Collections.emptyList(); + } + + return organizationService.findPersonParentAndChildrenOf(person, OrganizationNature.ORGANIZATION); + */ + + return organizationService.findAllByNature(OrganizationNature.ORGANIZATION, null, null); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/HomeResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/HomeResource.java new file mode 100644 index 0000000..457fd16 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/HomeResource.java @@ -0,0 +1,45 @@ +package com.oguerreiro.resilient.web.rest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.ContentPage; +import com.oguerreiro.resilient.domain.enumeration.ContentPageType; +import com.oguerreiro.resilient.repository.ContentPageRepository; +import com.oguerreiro.resilient.security.SecurityUtils; + +@RestController +@RequestMapping("/api/home") +public class HomeResource { + private final Logger log = LoggerFactory.getLogger(HomeResource.class); + + private final ContentPageRepository contentPageRepository; + + public HomeResource(ContentPageRepository contentPageRepository) { + this.contentPageRepository = contentPageRepository; + } + + /** + * Get home page HTML. Checks if user is authenticated to give the correct home page + * + * @return + */ + @GetMapping(value = "/page", produces = MediaType.TEXT_PLAIN_VALUE) + public String home() { + log.debug("REST request to get anonymous home page"); + + ContentPage home = null; + if (SecurityUtils.isAuthenticated()) { + home = this.contentPageRepository.findBySlug(ContentPageType.HOME_AUTHENTICATED); + } else { + home = this.contentPageRepository.findBySlug(ContentPageType.HOME_ANONYMOUS); + } + + return home.getContent(); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/InputDataResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/InputDataResource.java new file mode 100644 index 0000000..bcbdc7e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/InputDataResource.java @@ -0,0 +1,226 @@ +package com.oguerreiro.resilient.web.rest; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Sort; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.service.InputDataService; +import com.oguerreiro.resilient.service.criteria.InputDataCriteria; +import com.oguerreiro.resilient.service.dto.InputDataDTO; +import com.oguerreiro.resilient.service.query.InputDataQueryService; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.InputData}. + */ +@RestController +@RequestMapping("/api/input-data") +public class InputDataResource { + + private final Logger log = LoggerFactory.getLogger(InputDataResource.class); + + private static final String ENTITY_NAME = "inputData"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final InputDataService inputDataService; + + private final InputDataRepository inputDataRepository; + + private final InputDataQueryService inputDataQueryService; + + public InputDataResource(InputDataService inputDataService, InputDataRepository inputDataRepository, + InputDataQueryService inputDataQueryService) { + this.inputDataService = inputDataService; + this.inputDataRepository = inputDataRepository; + this.inputDataQueryService = inputDataQueryService; + } + + /** + * {@code POST /input-data} : Create a new inputData. + * + * @param inputDataDTO the inputDataDTO to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new + * inputDataDTO, or with status {@code 400 (Bad Request)} if the inputData has already an + * ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("") + public ResponseEntity createInputData(@Valid @RequestBody InputDataDTO inputDataDTO) + throws URISyntaxException { + log.debug("REST request to save InputData : {}", inputDataDTO); + if (inputDataDTO.getId() != null) { + throw new BadRequestAlertException("A new inputData cannot already have an ID", ENTITY_NAME, "idexists"); + } + inputDataDTO = inputDataService.save(inputDataDTO); + return ResponseEntity.created(new URI("/api/input-data/" + inputDataDTO.getId())).headers( + HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, inputDataDTO.getId().toString())).body( + inputDataDTO); + } + + /** + * {@code PUT /input-data/:id} : Updates an existing inputData. + * + * @param id the id of the inputDataDTO to save. + * @param inputDataDTO the inputDataDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated + * inputDataDTO, + * or with status {@code 400 (Bad Request)} if the inputDataDTO is not valid, + * or with status {@code 500 (Internal Server Error)} if the inputDataDTO couldn't be + * updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/{id}") + public ResponseEntity updateInputData(@PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody InputDataDTO inputDataDTO) throws URISyntaxException { + log.debug("REST request to update InputData : {}, {}", id, inputDataDTO); + if (inputDataDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, inputDataDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!inputDataRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + inputDataDTO = inputDataService.update(inputDataDTO); + return ResponseEntity.ok().headers( + HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, inputDataDTO.getId().toString())).body( + inputDataDTO); + } + + /** + * {@code PATCH /input-data/:id} : Partial updates given fields of an existing inputData, field will ignore if it is + * null + * + * @param id the id of the inputDataDTO to save. + * @param inputDataDTO the inputDataDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated + * inputDataDTO, + * or with status {@code 400 (Bad Request)} if the inputDataDTO is not valid, + * or with status {@code 404 (Not Found)} if the inputDataDTO is not found, + * or with status {@code 500 (Internal Server Error)} if the inputDataDTO couldn't be + * updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/{id}", consumes = { "application/json", "application/merge-patch+json" }) + public ResponseEntity partialUpdateInputData( + @PathVariable(value = "id", required = false) final Long id, @NotNull @RequestBody InputDataDTO inputDataDTO) + throws URISyntaxException { + log.debug("REST request to partial update InputData partially : {}, {}", id, inputDataDTO); + if (inputDataDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, inputDataDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!inputDataRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = inputDataService.partialUpdate(inputDataDTO); + + return ResponseUtil.wrapOrNotFound(result, + HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, inputDataDTO.getId().toString())); + } + + /** + * {@code GET /input-data} : get all the inputData. + * + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of inputData in body. + */ + @GetMapping("") + public ResponseEntity> getAllInputData(InputDataCriteria criteria) { + log.debug("REST request to get InputData by criteria: {}", criteria); + + Sort sort = Sort.by(Sort.Order.asc("variable.code")); + List entityList = inputDataQueryService.findByCriteria(criteria, sort); + return ResponseEntity.ok().body(entityList); + } + + /** + * {@code GET /input-data/count} : count all the inputData. + * + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the count in body. + */ + @GetMapping("/count") + public ResponseEntity countInputData(InputDataCriteria criteria) { + log.debug("REST request to count InputData by criteria: {}", criteria); + return ResponseEntity.ok().body(inputDataQueryService.countByCriteria(criteria)); + } + + /** + * {@code GET /input-data/:id} : get the "id" inputData. + * + * @param id the id of the inputDataDTO to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the inputDataDTO, or with status + * {@code 404 (Not Found)}. + */ + @GetMapping("/{id}") + public ResponseEntity getInputData(@PathVariable("id") Long id) { + log.debug("REST request to get InputData : {}", id); + Optional inputDataDTO = inputDataService.findOne(id); + return ResponseUtil.wrapOrNotFound(inputDataDTO); + } + + /** + * {@code DELETE /input-data/:id} : delete the "id" inputData. + * + * @param id the id of the inputDataDTO to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteInputData(@PathVariable("id") Long id) { + log.debug("REST request to delete InputData : {}", id); + inputDataService.delete(id); + return ResponseEntity.noContent().headers( + HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString())).build(); + } + + @GetMapping("/export/{periodId}") + public ResponseEntity export(@PathVariable("periodId") Long periodId) { + log.debug("REST request to export : {}", periodId); + + byte[] fileContent = this.inputDataService.export(periodId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDisposition(ContentDisposition.attachment().filename("report.xlsx").build()); + + return new ResponseEntity<>(fileContent, headers, HttpStatus.OK); + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/InputDataUploadLogResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/InputDataUploadLogResource.java new file mode 100644 index 0000000..085cdc2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/InputDataUploadLogResource.java @@ -0,0 +1,103 @@ +package com.oguerreiro.resilient.web.rest; + +import java.net.URISyntaxException; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.InputDataUploadLog; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.service.InputDataUploadLogService; +import com.oguerreiro.resilient.service.dto.InputDataUploadLogDTO; + +import jakarta.validation.Valid; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.InputDataUploadLog}. + */ +@RestController +@RequestMapping("/api/input-data-upload-logs") +public class InputDataUploadLogResource + extends AbstractResilientResource { + + private static final String ENTITY_NAME = "inputDataUploadLog"; + + private final InputDataUploadLogService inputDataUploadLogService; + + private final InputDataUploadLogRepository inputDataUploadLogRepository; + + public InputDataUploadLogResource(InputDataUploadLogService inputDataUploadLogService, + InputDataUploadLogRepository inputDataUploadLogRepository) { + super(InputDataUploadLog.class, inputDataUploadLogRepository, inputDataUploadLogService); + this.inputDataUploadLogService = inputDataUploadLogService; + this.inputDataUploadLogRepository = inputDataUploadLogRepository; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* Override and Unsupport standard endpoints */ + /** + * {@code POST /input-data-upload-logs} : Create a new entity. + * UNSUPPORTED. CANT CREATE InputDataUploadLog FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @PostMapping("") + public ResponseEntity create(@Valid InputDataUploadLogDTO domainDTO) + throws URISyntaxException { + log.debug("REST request to create InputDataUploadLog : {}", domainDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code PUT /input-data-upload-logs/:id} : Updates an existing entity. + * UNSUPPORTED. CANT UPDATE InputDataUploadLog FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @PutMapping("/{id}") + public ResponseEntity update(Long id, @Valid InputDataUploadLogDTO domainDTO) + throws URISyntaxException { + log.debug("REST request to update InputDataUploadLog : {}", id, domainDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code DELETE /input-data-upload-logs/:id} : delete entity by the "id". + * UNSUPPORTED. CANT DELETE InputDataUploadLog FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @DeleteMapping("/{id}") + public ResponseEntity delete(Long id) { + log.debug("REST request to delete InputDataUploadLog : {}", id); + throw new UnsupportedOperationException(); + } + + /* InputDataUploadLogResource endpoints */ + /** + * {@code GET /input-data-upload-logs} : get all the inputDataUploadLogs. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of inputDataUploadLogs in + * body. + */ + @GetMapping("/owner/{id}") + public List getAllInputDataUploadLogsByOwner( + @PathVariable(value = "id", required = false) final Long id) { + log.debug("REST request to get all InputDataUploadLogs By Owner : {}", id); + return inputDataUploadLogService.findAllByOwner(id); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/InputDataUploadResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/InputDataUploadResource.java new file mode 100644 index 0000000..6d8962f --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/InputDataUploadResource.java @@ -0,0 +1,91 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.InputDataUploadRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.service.InputDataUploadService; +import com.oguerreiro.resilient.service.OrganizationService; +import com.oguerreiro.resilient.service.dto.InputDataUploadDTO; + +import tech.jhipster.web.util.HeaderUtil; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.InputDataUpload}. + */ +@RestController +@RequestMapping("/api/input-data-uploads") +public class InputDataUploadResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "inputDataUpload"; + + private final InputDataUploadService inputDataUploadService; + private final OrganizationService organizationService; + private final UserRepository userRepository; + + private final InputDataUploadRepository inputDataUploadRepository; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + public InputDataUploadResource(InputDataUploadService inputDataUploadService, + InputDataUploadRepository inputDataUploadRepository, OrganizationService organizationService, + UserRepository userRepository) { + super(InputDataUpload.class, inputDataUploadRepository, inputDataUploadService); + this.inputDataUploadService = inputDataUploadService; + this.inputDataUploadRepository = inputDataUploadRepository; + this.organizationService = organizationService; + this.userRepository = userRepository; + } + + /* Overriden methods */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @GetMapping("") + public List getAll() { + log.debug("REST request to get all " + this.getClass().getSimpleName() + "'s."); + + // Calculate the list of Organizations that it can see + //Get current authenticated user + User currentUser = SecurityUtils.getCurrentUserLogin().flatMap(this.userRepository::findOneByLogin).orElseThrow(); + + List organizations = organizationService.getOrganizationsForUserName(currentUser); + + return this.inputDataUploadService.findAllOwned(organizations); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /** + * {@code GET /input-data-uploads} : get all the inputDataUploads. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of inputDataUploads in body. + */ + @GetMapping("/{id}/doLoad") + public ResponseEntity doLoad(@PathVariable("id") Long id) { + log.debug("REST request to doload() for InputDataUpload id = " + id); + + // Execute load + inputDataUploadService.doLoad(id); + + return ResponseEntity.noContent().headers( + HeaderUtil.createAlert(applicationName, "Import has started ...", id.toString())).build(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/InventoryResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/InventoryResource.java new file mode 100644 index 0000000..478dd6e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/InventoryResource.java @@ -0,0 +1,87 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.service.InputDataService; +import com.oguerreiro.resilient.service.OrganizationService; +import com.oguerreiro.resilient.service.criteria.InputDataCriteria; +import com.oguerreiro.resilient.service.dto.InventoryDataDTO; +import com.oguerreiro.resilient.service.mapper.InventoryDataMapper; +import com.oguerreiro.resilient.service.query.InputDataQueryService; + +/** + * REST controller for managing Inventory requests. Inventory is a POJO for frontend InpuData view. + */ +@RestController +@RequestMapping("/api/inventory") +public class InventoryResource { + + private final Logger log = LoggerFactory.getLogger(InventoryResource.class); + + private static final String ENTITY_NAME = "inventory"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final InputDataService inputDataService; + + public InventoryResource(InputDataRepository inputDataRepository, InputDataQueryService inputDataQueryService, + InventoryDataMapper inventoryDataMapper, InputDataService inputDataService, + OrganizationService organizationService) { + this.inputDataService = inputDataService; + + } + + /** + * {@code GET /inventory} : get all the outputData. + * + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of inventory in body. + */ + @PreAuthorize("@resilientSecurity.canAccess('READ', 'com.oguerreiro.resilient.domain.InputData')") // Inventory, delegates permission to InputData + @GetMapping("") + public ResponseEntity> searchInventory(InputDataCriteria criteria) { + log.debug("REST request to searchInventory by criteria: {}", criteria); + + Long periodId = criteria.getPeriodId() == null ? null : criteria.getPeriodId().getEquals(); + Long ownerId = criteria.getOwnerId() == null ? null : criteria.getOwnerId().getEquals(); + Long scopeId = criteria.getScopeId() == null ? null : criteria.getScopeId().getEquals(); + Long categoryId = criteria.getCategoryId() == null ? null : criteria.getCategoryId().getEquals(); + + List entityList = inputDataService.searchInventory(periodId, ownerId, scopeId, categoryId); + return ResponseEntity.ok().body(entityList); + } + + @PreAuthorize("@resilientSecurity.canAccess('READ', 'com.oguerreiro.resilient.domain.InputData')") // Inventory, delegates permission to InputData + @GetMapping("/export") + public ResponseEntity export(InputDataCriteria criteria) { + log.debug("REST request to export : {}"); + + Long periodId = criteria.getPeriodId() == null ? null : criteria.getPeriodId().getEquals(); + Long ownerId = criteria.getOwnerId() == null ? null : criteria.getOwnerId().getEquals(); + Long scopeId = criteria.getScopeId() == null ? null : criteria.getScopeId().getEquals(); + Long categoryId = criteria.getCategoryId() == null ? null : criteria.getCategoryId().getEquals(); + + byte[] fileContent = this.inputDataService.exportInventory(periodId, ownerId, scopeId, categoryId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDisposition(ContentDisposition.attachment().filename("report.xlsx").build()); + + return new ResponseEntity<>(fileContent, headers, HttpStatus.OK); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/MetadataPropertyResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/MetadataPropertyResource.java new file mode 100644 index 0000000..418bc76 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/MetadataPropertyResource.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.web.rest; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.MetadataProperty; +import com.oguerreiro.resilient.repository.MetadataPropertyRepository; +import com.oguerreiro.resilient.service.MetadataPropertyService; +import com.oguerreiro.resilient.service.dto.MetadataPropertyDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.MetadataProperty}. + */ +@RestController +@RequestMapping("/api/metadata-properties") +public class MetadataPropertyResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "metadataProperty"; + + public MetadataPropertyResource(MetadataPropertyService metadataPropertyService, + MetadataPropertyRepository metadataPropertyRepository) { + super(MetadataProperty.class, metadataPropertyRepository, metadataPropertyService); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/MetadataValueResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/MetadataValueResource.java new file mode 100644 index 0000000..e84fb79 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/MetadataValueResource.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.web.rest; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.MetadataValue; +import com.oguerreiro.resilient.repository.MetadataValueRepository; +import com.oguerreiro.resilient.service.MetadataValueService; +import com.oguerreiro.resilient.service.dto.MetadataValueDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.MetadataValue}. + */ +@RestController +@RequestMapping("/api/metadata-values") +public class MetadataValueResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "metadataValue"; + + public MetadataValueResource(MetadataValueService metadataValueService, + MetadataValueRepository metadataValueRepository) { + super(MetadataValue.class, metadataValueRepository, metadataValueService); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationResource.java new file mode 100644 index 0000000..c013ecb --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationResource.java @@ -0,0 +1,94 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.service.OrganizationService; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Organization}. + */ +@RestController +@RequestMapping("/api/organizations") +public class OrganizationResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "organization"; + private final OrganizationService organizationService; + + public OrganizationResource(OrganizationService organizationService, OrganizationRepository organizationRepository) { + super(Organization.class, organizationRepository, organizationService); + this.organizationService = organizationService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* OrganizationResource endpoints */ + /** + * {@code GET /organizations/byNature/{nature} : get all the Organization within the provided nature. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of Organizations in body. + */ + @PreAuthorize("@resilientSecurity.canAccess('READ')") // Must have READ permission + @GetMapping("/byNature/{nature}") + public List getAllByNature(@PathVariable("nature") OrganizationNature nature) { + log.debug("REST request to get all getAllByNature : {}", nature); + return organizationService.findAllByNature(nature, null, null); + } + + /** + * {@code GET /organizations/byNature/forInput/{nature} : get all the Organization within the provided nature. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of Organizations in body. + */ + @PreAuthorize("@resilientSecurity.canAccess('READ')") // Must have READ permission + @GetMapping("/byNature/forInput/{nature}") + public List getAllByNatureForInput(@PathVariable("nature") OrganizationNature nature) { + log.debug("REST request to get all getAllByNature : {}", nature); + return organizationService.findAllByNature(nature, Boolean.TRUE, null); + } + + /** + * {@code GET /organizations/hierarchy/{nature} : get all the Organization within the provided nature, that the user + * has access (create/update) to. + * @param nature + * + * @return + */ + @PreAuthorize("@resilientSecurity.canAccess('READ')") // Must have READ permission + @GetMapping("/hierarchy/{nature}") + public List getAllByNatureForUserHierarchy(@PathVariable("nature") OrganizationNature nature) { + log.debug("REST request to get all getAllByNatureForUserHierarchy : {}", nature); + + return organizationService.findAllByNatureForUserHierarchy(nature, null, null); + } + + /** + * {@code GET /organizations/hierarchy/{nature} : get all the Organization within the provided nature, that the user + * has access (create/update) to. + * @param nature + * + * @return + */ + @PreAuthorize("@resilientSecurity.canAccess('READ')") // Must have READ permission + @GetMapping("/hierarchy/forInput/{nature}") + public List getAllByNatureForUserHierarchyForInput( + @PathVariable("nature") OrganizationNature nature) { + log.debug("REST request to get all getAllByNatureForUserHierarchy : {}", nature); + + return organizationService.findAllByNatureForUserHierarchy(nature, Boolean.TRUE, null); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationTypeExtendedResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationTypeExtendedResource.java new file mode 100644 index 0000000..673e6b6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationTypeExtendedResource.java @@ -0,0 +1,74 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.repository.OrganizationTypeExtendedRepository; +import com.oguerreiro.resilient.service.OrganizationTypeExtendedService; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.OrganizationType}. + */ +@RestController +@RequestMapping("/api/organization-types") +public class OrganizationTypeExtendedResource { + + private final Logger log = LoggerFactory.getLogger(OrganizationTypeExtendedResource.class); + + private static final String ENTITY_NAME = "organizationType"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final OrganizationTypeExtendedService organizationTypeService; + + private final OrganizationTypeExtendedRepository organizationTypeRepository; + + public OrganizationTypeExtendedResource( + OrganizationTypeExtendedService organizationTypeService, + OrganizationTypeExtendedRepository organizationTypeRepository + ) { + this.organizationTypeService = organizationTypeService; + this.organizationTypeRepository = organizationTypeRepository; + } + + /** + * {@code GET /organization-types} : get all the organizationTypes. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of organizationTypes in body. + */ + @GetMapping("/nature/{nature}") + public List getOrganizationTypesForNature ( + @PathVariable("nature") String nature + ) { + log.debug("REST request to get all OrganizationTypes"); + + OrganizationNature parentNature = OrganizationNature.valueOf(nature); + + if (parentNature == null) { + //Invalid request. Nature unknown. + throw new BadRequestAlertException("Nature not found", ENTITY_NAME, "idnotfound"); + } + + List natures = new ArrayList(); + natures.add(OrganizationNature.PERSON); + natures.add(OrganizationNature.LEVEL); + if (parentNature.equals(OrganizationNature.ORGANIZATION) || parentNature.equals(OrganizationNature.LEVEL)) natures.add(OrganizationNature.ORGANIZATION); + if (parentNature.equals(OrganizationNature.ORGANIZATION) || parentNature.equals(OrganizationNature.LEVEL)) natures.add(OrganizationNature.FACILITY); + + return organizationTypeService.findByNatureIn(natures); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationTypeResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationTypeResource.java new file mode 100644 index 0000000..7463244 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/OrganizationTypeResource.java @@ -0,0 +1,178 @@ +package com.oguerreiro.resilient.web.rest; + +import com.oguerreiro.resilient.repository.OrganizationTypeRepository; +import com.oguerreiro.resilient.service.OrganizationTypeService; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.OrganizationType}. + */ +@RestController +@RequestMapping("/api/organization-types") +public class OrganizationTypeResource { + + private final Logger log = LoggerFactory.getLogger(OrganizationTypeResource.class); + + private static final String ENTITY_NAME = "organizationType"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final OrganizationTypeService organizationTypeService; + + private final OrganizationTypeRepository organizationTypeRepository; + + public OrganizationTypeResource( + OrganizationTypeService organizationTypeService, + OrganizationTypeRepository organizationTypeRepository + ) { + this.organizationTypeService = organizationTypeService; + this.organizationTypeRepository = organizationTypeRepository; + } + + /** + * {@code POST /organization-types} : Create a new organizationType. + * + * @param organizationTypeDTO the organizationTypeDTO to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new organizationTypeDTO, or with status {@code 400 (Bad Request)} if the organizationType has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("") + public ResponseEntity createOrganizationType(@Valid @RequestBody OrganizationTypeDTO organizationTypeDTO) + throws URISyntaxException { + log.debug("REST request to save OrganizationType : {}", organizationTypeDTO); + if (organizationTypeDTO.getId() != null) { + throw new BadRequestAlertException("A new organizationType cannot already have an ID", ENTITY_NAME, "idexists"); + } + organizationTypeDTO = organizationTypeService.save(organizationTypeDTO); + return ResponseEntity.created(new URI("/api/organization-types/" + organizationTypeDTO.getId())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, organizationTypeDTO.getId().toString())) + .body(organizationTypeDTO); + } + + /** + * {@code PUT /organization-types/:id} : Updates an existing organizationType. + * + * @param id the id of the organizationTypeDTO to save. + * @param organizationTypeDTO the organizationTypeDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated organizationTypeDTO, + * or with status {@code 400 (Bad Request)} if the organizationTypeDTO is not valid, + * or with status {@code 500 (Internal Server Error)} if the organizationTypeDTO couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/{id}") + public ResponseEntity updateOrganizationType( + @PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody OrganizationTypeDTO organizationTypeDTO + ) throws URISyntaxException { + log.debug("REST request to update OrganizationType : {}, {}", id, organizationTypeDTO); + if (organizationTypeDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, organizationTypeDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!organizationTypeRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + organizationTypeDTO = organizationTypeService.update(organizationTypeDTO); + return ResponseEntity.ok() + .headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, organizationTypeDTO.getId().toString())) + .body(organizationTypeDTO); + } + + /** + * {@code PATCH /organization-types/:id} : Partial updates given fields of an existing organizationType, field will ignore if it is null + * + * @param id the id of the organizationTypeDTO to save. + * @param organizationTypeDTO the organizationTypeDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated organizationTypeDTO, + * or with status {@code 400 (Bad Request)} if the organizationTypeDTO is not valid, + * or with status {@code 404 (Not Found)} if the organizationTypeDTO is not found, + * or with status {@code 500 (Internal Server Error)} if the organizationTypeDTO couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/{id}", consumes = { "application/json", "application/merge-patch+json" }) + public ResponseEntity partialUpdateOrganizationType( + @PathVariable(value = "id", required = false) final Long id, + @NotNull @RequestBody OrganizationTypeDTO organizationTypeDTO + ) throws URISyntaxException { + log.debug("REST request to partial update OrganizationType partially : {}, {}", id, organizationTypeDTO); + if (organizationTypeDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, organizationTypeDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!organizationTypeRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = organizationTypeService.partialUpdate(organizationTypeDTO); + + return ResponseUtil.wrapOrNotFound( + result, + HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, organizationTypeDTO.getId().toString()) + ); + } + + /** + * {@code GET /organization-types} : get all the organizationTypes. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of organizationTypes in body. + */ + @GetMapping("") + public List getAllOrganizationTypes( + @RequestParam(name = "eagerload", required = false, defaultValue = "true") boolean eagerload + ) { + log.debug("REST request to get all OrganizationTypes"); + return organizationTypeService.findAll(); + } + + /** + * {@code GET /organization-types/:id} : get the "id" organizationType. + * + * @param id the id of the organizationTypeDTO to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the organizationTypeDTO, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/{id}") + public ResponseEntity getOrganizationType(@PathVariable("id") Long id) { + log.debug("REST request to get OrganizationType : {}", id); + Optional organizationTypeDTO = organizationTypeService.findOne(id); + return ResponseUtil.wrapOrNotFound(organizationTypeDTO); + } + + /** + * {@code DELETE /organization-types/:id} : delete the "id" organizationType. + * + * @param id the id of the organizationTypeDTO to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteOrganizationType(@PathVariable("id") Long id) { + log.debug("REST request to delete OrganizationType : {}", id); + organizationTypeService.delete(id); + return ResponseEntity.noContent() + .headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString())) + .build(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/OutputDataResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/OutputDataResource.java new file mode 100644 index 0000000..090f3a7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/OutputDataResource.java @@ -0,0 +1,121 @@ +package com.oguerreiro.resilient.web.rest; + +import java.net.URISyntaxException; + +import org.springframework.data.domain.Sort; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.repository.OutputDataRepository; +import com.oguerreiro.resilient.service.OutputDataService; +import com.oguerreiro.resilient.service.criteria.OutputDataCriteria; +import com.oguerreiro.resilient.service.dto.OutputDataDTO; +import com.oguerreiro.resilient.service.query.OutputDataQueryService; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +/** + * READONLY + * REST controller for managing {@link com.oguerreiro.resilient.domain.OutputData}. + */ +@RestController +@RequestMapping("/api/output-data") +public class OutputDataResource + extends AbstractResilientQueryResource { + + private static final String ENTITY_NAME = "outputData"; + private final OutputDataService outputDataService; + + public OutputDataResource(OutputDataRepository outputDataRepository, OutputDataService outputDataService, + OutputDataQueryService outputDataQueryService) { + super(OutputData.class, outputDataRepository, outputDataService, outputDataQueryService); + this.outputDataService = outputDataService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + @Override + protected Sort getAllSort() { + return Sort.by(Sort.Order.asc("variable.code")); + } + + /* Override and Unsupport standard endpoints */ + /** + * {@code POST /output-data} : Create a new outputData. + * UNSUPPORTED. CANT CREATE OUTPUT DATA FROM FRONTEND + */ + @Override + @PostMapping("") + public ResponseEntity create(@Valid @RequestBody OutputDataDTO outputDataDTO) + throws URISyntaxException { + log.debug("REST request to save OutputData : {}", outputDataDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code PUT /output-data/:id} : Updates an existing outputData. + * UNSUPPORTED. CANT UPDATE OUTPUT DATA FROM FRONTEND + */ + @Override + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody OutputDataDTO outputDataDTO) throws URISyntaxException { + log.debug("REST request to update OutputData : {}, {}", id, outputDataDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code PATCH /output-data/:id} : Partial updates given fields of an existing outputData, field will ignore if it + * is null + * UNSUPPORTED. CANT UPDATE OUTPUT DATA FROM FRONTEND + */ + @Override + @PatchMapping(value = "/{id}", consumes = { "application/json", "application/merge-patch+json" }) + public ResponseEntity partialUpdate(@PathVariable(value = "id", required = false) final Long id, + @NotNull @RequestBody OutputDataDTO outputDataDTO) throws URISyntaxException { + log.debug("REST request to partial update OutputData partially : {}, {}", id, outputDataDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code DELETE /output-data/:id} : delete the "id" outputData. + * UNSUPPORTED. CANT DELETE OUTPUT DATA FROM FRONTEND + */ + @Override + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable("id") Long id) { + log.debug("REST request to delete OutputData : {}", id); + throw new UnsupportedOperationException(); + } + + @GetMapping("/export/{periodVersionId}") + public ResponseEntity export(@PathVariable("periodVersionId") Long periodVersionId) { + log.debug("REST request to export : {}"); + + byte[] fileContent = this.outputDataService.export(periodVersionId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDisposition(ContentDisposition.attachment().filename("report.xlsx").build()); + + return new ResponseEntity<>(fileContent, headers, HttpStatus.OK); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/PeriodResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/PeriodResource.java new file mode 100644 index 0000000..d1e458d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/PeriodResource.java @@ -0,0 +1,67 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.repository.PeriodRepository; +import com.oguerreiro.resilient.service.PeriodService; +import com.oguerreiro.resilient.service.PeriodVersionService; +import com.oguerreiro.resilient.service.dto.PeriodDTO; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Period}. + */ +@RestController +@RequestMapping("/api/periods") +public class PeriodResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "period"; + + private final PeriodVersionService periodVersionService; + private final PeriodService periodService; + + public PeriodResource(PeriodService periodService, PeriodRepository periodRepository, + PeriodVersionService periodVersionService) { + super(Period.class, periodRepository, periodService); + this.periodVersionService = periodVersionService; + this.periodService = periodService; + } + + /* AbstractResilientResource methods impl*/ + @Override + protected String getEntityName() { + return PeriodResource.ENTITY_NAME; + } + + /* PeriodResource custom endpoints */ + @GetMapping("/{periodId}/versions") + public List getAllByPeriod(@PathVariable("periodId") Long periodId) { + log.debug("REST request to get all getAllByPeriod Id : {}", periodId); + return periodVersionService.findAllByPeriod(periodId); + } + + @GetMapping("/{periodId}/lastVersion") + public PeriodVersionDTO getLatestVersionForPeriod(@PathVariable("periodId") Long periodId) { + log.debug("REST request to get all getLatestVersionForPeriod Id : {}", periodId); + return periodVersionService.findLatestVersionForPeriod(periodId); + } + + @GetMapping("/{periodVersionId}/version") + public PeriodVersionDTO getVersion(@PathVariable("periodVersionId") Long periodVersionId) { + log.debug("REST request to get all getVersion Id : {}", periodVersionId); + return periodVersionService.findOne(periodVersionId).orElseThrow(); + } + + @GetMapping("/byState/{state}") + public List getAllByState(@PathVariable("state") PeriodStatus state) { + log.debug("REST request to getAllByStatus : {}", state); + return periodService.findAllByState(state); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/PeriodVersionResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/PeriodVersionResource.java new file mode 100644 index 0000000..6d94956 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/PeriodVersionResource.java @@ -0,0 +1,93 @@ +package com.oguerreiro.resilient.web.rest; + +import java.net.URISyntaxException; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; + +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.repository.PeriodVersionRepository; +import com.oguerreiro.resilient.service.PeriodVersionService; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; + +import jakarta.validation.Valid; + +/** + * REST controller for managing + * {@link com.oguerreiro.resilient.domain.PeriodVersion}. + */ +//@RestController +//@RequestMapping("/api/period-versions") +public class PeriodVersionResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "periodVersion"; + + private final PeriodVersionService periodVersionService; + + public PeriodVersionResource(PeriodVersionService periodVersionService, + PeriodVersionRepository periodVersionRepository) { + super(PeriodVersion.class, periodVersionRepository, periodVersionService); + this.periodVersionService = periodVersionService; + } + + /* AbstractResilientResource methods impl*/ + @Override + protected String getEntityName() { + return PeriodVersionResource.ENTITY_NAME; + } + + /* Override and Unsupport standard endpoints */ + /** + * {@code POST /period-versions} : Create a new entity. + * UNSUPPORTED. CANT CREATE PeriodVersion's FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @PostMapping("") + public ResponseEntity create(@Valid PeriodVersionDTO domainDTO) throws URISyntaxException { + log.debug("REST request to create PeriodVersion : {}", domainDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code PUT /period-versions/:id} : Updates an existing entity. + * UNSUPPORTED. CANT UPDATE PeriodVersion's FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @PutMapping("/{id}") + public ResponseEntity update(Long id, @Valid PeriodVersionDTO domainDTO) throws URISyntaxException { + log.debug("REST request to update PeriodVersion : {}", domainDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code DELETE /period-versions/:id} : delete entity by the "id". + * UNSUPPORTED. CANT DELETE PeriodVersion's FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @DeleteMapping("/{id}") + public ResponseEntity delete(Long id) { + log.debug("REST request to delete PeriodVersion"); + throw new UnsupportedOperationException(); + } + + /* PeriodVersionResource endpoints */ + /** + * {@code GET /period-versions/byPeriod/:periodId} : get all periodVersions for the giver periodId. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of units in body. + */ + @GetMapping("/byPeriod/{periodId}") + public List getAllByPeriod(@PathVariable("periodId") Long periodId) { + log.debug("REST request to get all getAllByPeriod Id : {}", periodId); + return periodVersionService.findAllByPeriod(periodId); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/PublicUserResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/PublicUserResource.java new file mode 100644 index 0000000..d44d67a --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/PublicUserResource.java @@ -0,0 +1,56 @@ +package com.oguerreiro.resilient.web.rest; + +import com.oguerreiro.resilient.service.UserService; +import com.oguerreiro.resilient.service.dto.UserDTO; +import java.util.*; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.PaginationUtil; + +@RestController +@RequestMapping("/api") +public class PublicUserResource { + + private static final List ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList( + Arrays.asList("id", "login", "firstName", "lastName", "email", "activated", "langKey") + ); + + private final Logger log = LoggerFactory.getLogger(PublicUserResource.class); + + private final UserService userService; + + public PublicUserResource(UserService userService) { + this.userService = userService; + } + + /** + * {@code GET /users} : get all users with only public information - calling this method is allowed for anyone. + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users. + */ + @GetMapping("/users") + public ResponseEntity> getAllPublicUsers(@org.springdoc.core.annotations.ParameterObject Pageable pageable) { + log.debug("REST request to get all public User names"); + if (!onlyContainsAllowedProperties(pageable)) { + return ResponseEntity.badRequest().build(); + } + + final Page page = userService.getAllPublicUsers(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + private boolean onlyContainsAllowedProperties(Pageable pageable) { + return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/ResilientLogResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/ResilientLogResource.java new file mode 100644 index 0000000..e20067d --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/ResilientLogResource.java @@ -0,0 +1,99 @@ +package com.oguerreiro.resilient.web.rest; + +import java.net.URISyntaxException; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.ResilientLog; +import com.oguerreiro.resilient.repository.ResilientLogRepository; +import com.oguerreiro.resilient.service.ResilientLogService; +import com.oguerreiro.resilient.service.dto.ResilientLogDTO; + +import jakarta.validation.Valid; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.ResilientLog}. + */ +@RestController +@RequestMapping("/api/resilient-logs") +public class ResilientLogResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "resilientLog"; + + private final ResilientLogService resilientLogService; + + private final ResilientLogRepository resilientLogRepository; + + public ResilientLogResource(ResilientLogService inputDataUploadLogService, + ResilientLogRepository inputDataUploadLogRepository) { + super(ResilientLog.class, inputDataUploadLogRepository, inputDataUploadLogService); + this.resilientLogService = inputDataUploadLogService; + this.resilientLogRepository = inputDataUploadLogRepository; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* Override and Unsupport standard endpoints */ + /** + * {@code POST /resilient-logs} : Create a new entity. + * UNSUPPORTED. CANT CREATE ResilientLog FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @PostMapping("") + public ResponseEntity create(@Valid ResilientLogDTO domainDTO) throws URISyntaxException { + log.debug("REST request to create ResilientLog : {}", domainDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code PUT /resilient-logs/:id} : Updates an existing entity. + * UNSUPPORTED. CANT UPDATE ResilientLog FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @PutMapping("/{id}") + public ResponseEntity update(Long id, @Valid ResilientLogDTO domainDTO) throws URISyntaxException { + log.debug("REST request to update ResilientLog : {}", id, domainDTO); + throw new UnsupportedOperationException(); + } + + /** + * {@code DELETE /resilient-logs/:id} : delete entity by the "id". + * UNSUPPORTED. CANT DELETE ResilientLog FROM FRONTEND + */ + @Override + @PreAuthorize("@resilientSecurity.canAccess()") + @DeleteMapping("/{id}") + public ResponseEntity delete(Long id) { + log.debug("REST request to delete ResilientLog : {}", id); + throw new UnsupportedOperationException(); + } + + /* ResilientLogResource endpoints */ + /** + * {@code GET /resilient-logs} : get all the resilientLogs. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of resilientLogs in + * body. + */ + @GetMapping("/owner/{id}") + public List getAllResilientLogsByOwner(@PathVariable(value = "id", required = false) final Long id) { + log.debug("REST request to get all ResilientLogs By Owner : {}", id); + return resilientLogService.findAllByOwnerId(id); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/UnitConverterResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/UnitConverterResource.java new file mode 100644 index 0000000..e932699 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/UnitConverterResource.java @@ -0,0 +1,75 @@ +package com.oguerreiro.resilient.web.rest; + +import java.math.BigDecimal; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.error.VariableUnitConverterMoreThanOneFoundException; +import com.oguerreiro.resilient.error.VariableUnitConverterNotFoundException; +import com.oguerreiro.resilient.error.VariableUnitConverterWithoutFormulaException; +import com.oguerreiro.resilient.repository.UnitConverterRepository; +import com.oguerreiro.resilient.service.UnitConverterService; +import com.oguerreiro.resilient.service.criteria.UnitConverterCriteria; +import com.oguerreiro.resilient.service.dto.UnitConverterDTO; +import com.oguerreiro.resilient.service.query.UnitConverterQueryService; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.UnitConverter}. + */ +@RestController +@RequestMapping("/api/unit-converters") +public class UnitConverterResource + extends AbstractResilientQueryResource { + + private static final String ENTITY_NAME = "unitConverter"; + + private final UnitConverterService unitConverterService; + + public UnitConverterResource(UnitConverterService unitConverterService, + UnitConverterRepository unitConverterRepository, UnitConverterQueryService unitConverterQueryService) { + super(UnitConverter.class, unitConverterRepository, unitConverterService, unitConverterQueryService); + + this.unitConverterService = unitConverterService; + } + + /* AbstractResilientResource methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* UnitConverterResource endpoints */ + /** + * {@code GET /unit-converters} : get all the unitConverters. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of unitConverters in body. + */ + @GetMapping("/from/{fromUnitId}/to/{toUnitId}") + public List getAllUnitConvertersFromAndTo(@PathVariable("fromUnitId") Long fromUnitId, + @PathVariable("toUnitId") Long toUnitId) { + log.debug("REST request to get all getAllUnitConvertersFromAndTo"); + List values = unitConverterService.findAllFromAndTo(fromUnitId, toUnitId); + return values; + } + + @GetMapping("/convert/{periodId}/{variableId}/{variableClassCode}/{fromUnitId}/{value}") + public ResponseEntity getConvert(@PathVariable("fromUnitId") Long fromUnitId, + @PathVariable("periodId") Long periodId, @PathVariable("variableId") Long variableId, + @PathVariable("variableClassCode") String variableClassCode, @PathVariable("value") BigDecimal value) + throws VariableUnitConverterNotFoundException, VariableUnitConverterWithoutFormulaException, + VariableUnitConverterMoreThanOneFoundException { + log.debug("REST request to get getConvert : {}", fromUnitId, periodId, variableId, variableClassCode, value); + + BigDecimal convertedValue = null; + convertedValue = unitConverterService.convertIfNeeded(periodId, variableId, fromUnitId, value, variableClassCode); + + return ResponseEntity.ok(convertedValue); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/UnitResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/UnitResource.java new file mode 100644 index 0000000..69b0fd6 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/UnitResource.java @@ -0,0 +1,69 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.service.UnitService; +import com.oguerreiro.resilient.service.criteria.UnitCriteria; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.query.UnitQueryService; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Unit}. + */ +@RestController +@RequestMapping("/api/units") +public class UnitResource extends AbstractResilientQueryResource { + + private static final String ENTITY_NAME = "unit"; + + private final UnitService unitService; + + private final UnitRepository unitRepository; + private final UnitQueryService unitQueryService; + + public UnitResource(UnitService unitService, UnitRepository unitRepository, UnitQueryService unitQueryService) { + super(Unit.class, unitRepository, unitService, unitQueryService); + this.unitService = unitService; + this.unitRepository = unitRepository; + this.unitQueryService = unitQueryService; + } + + /* AbstractResilientResource methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* UnitResource endpoints */ + + /** + * {@code GET /units/byType/:unitTypeId} : get all the units. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of units in body. + */ + @GetMapping("/byType/{unitTypeId}") + public List getAllUnitsByType(@PathVariable("unitTypeId") Long unitTypeId) { + log.debug("REST request to get all UnitsByType Id : {}", unitTypeId); + return unitService.findAllByType(unitTypeId); + } + + /** + * {@code GET /units/forVariable/:variableId} : get all the units allowed in variable. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of units in body. + */ + @GetMapping("/forVariable/{variableId}") + public List getAllUnitsAllowedForVariable(@PathVariable("variableId") Long variableId) { + log.debug("REST request to get all getAllUnitsAllowedForVariable Id : {}", variableId); + return unitService.findAllUnitsAllowedForVariable(variableId); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/UnitTypeResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/UnitTypeResource.java new file mode 100644 index 0000000..fbc90df --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/UnitTypeResource.java @@ -0,0 +1,32 @@ +package com.oguerreiro.resilient.web.rest; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.UnitType; +import com.oguerreiro.resilient.repository.UnitTypeRepository; +import com.oguerreiro.resilient.service.UnitTypeService; +import com.oguerreiro.resilient.service.dto.UnitTypeDTO; +import com.oguerreiro.resilient.service.mapper.UnitTypeMapper; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.UnitType}. + */ +@RestController +@RequestMapping("/api/unit-types") +public class UnitTypeResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "unitType"; + + public UnitTypeResource(UnitTypeRepository unitTypeRepository, UnitTypeService unitTypeService, + UnitTypeMapper unitTypeMapper) { + super(UnitType.class, unitTypeRepository, unitTypeService); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/UserResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/UserResource.java new file mode 100644 index 0000000..0fac73e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/UserResource.java @@ -0,0 +1,209 @@ +package com.oguerreiro.resilient.web.rest; + +import com.oguerreiro.resilient.config.Constants; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.service.MailService; +import com.oguerreiro.resilient.service.UserService; +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; +import com.oguerreiro.resilient.web.rest.errors.EmailAlreadyUsedException; +import com.oguerreiro.resilient.web.rest.errors.LoginAlreadyUsedException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Pattern; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.PaginationUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing users. + *

+ * This class accesses the {@link com.oguerreiro.resilient.domain.User} entity, and needs to fetch its collection of authorities. + *

+ * For a normal use-case, it would be better to have an eager relationship between User and Authority, + * and send everything to the client side: there would be no View Model and DTO, a lot less code, and an outer-join + * which would be good for performance. + *

+ * We use a View Model and a DTO for 3 reasons: + *

    + *
  • We want to keep a lazy association between the user and the authorities, because people will + * quite often do relationships with the user, and we don't want them to get the authorities all + * the time for nothing (for performance reasons). This is the #1 goal: we should not impact our users' + * application because of this use-case.
  • + *
  • Not having an outer join causes n+1 requests to the database. This is not a real issue as + * we have by default a second-level cache. This means on the first HTTP call we do the n+1 requests, + * but then all authorities come from the cache, so in fact it's much better than doing an outer join + * (which will get lots of data from the database, for each HTTP call).
  • + *
  • As this manages users, for security reasons, we'd rather have a DTO layer.
  • + *
+ *

+ * Another option would be to have a specific JPA entity graph to handle this case. + */ +@RestController +@RequestMapping("/api/admin") +public class UserResource { + + private static final List ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList( + Arrays.asList( + "id", + "login", + "firstName", + "lastName", + "email", + "activated", + "langKey", + "createdBy", + "createdDate", + "lastModifiedBy", + "lastModifiedDate" + ) + ); + + private final Logger log = LoggerFactory.getLogger(UserResource.class); + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final UserService userService; + + private final UserRepository userRepository; + + private final MailService mailService; + + public UserResource(UserService userService, UserRepository userRepository, MailService mailService) { + this.userService = userService; + this.userRepository = userRepository; + this.mailService = mailService; + } + + /** + * {@code POST /admin/users} : Creates a new user. + *

+ * Creates a new user if the login and email are not already used, and sends an + * mail with an activation link. + * The user needs to be activated on creation. + * + * @param userDTO the user to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new user, or with status {@code 400 (Bad Request)} if the login or email is already in use. + * @throws URISyntaxException if the Location URI syntax is incorrect. + * @throws BadRequestAlertException {@code 400 (Bad Request)} if the login or email is already in use. + */ + @PostMapping("/users") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity createUser(@Valid @RequestBody AdminUserDTO userDTO) throws URISyntaxException { + log.debug("REST request to save User : {}", userDTO); + + if (userDTO.getId() != null) { + throw new BadRequestAlertException("A new user cannot already have an ID", "userManagement", "idexists"); + // Lowercase the user login before comparing with database + } else if (userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()).isPresent()) { + throw new LoginAlreadyUsedException(); + } else if (userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()).isPresent()) { + throw new EmailAlreadyUsedException(); + } else { + User newUser = userService.createUser(userDTO); + mailService.sendCreationEmail(newUser); + return ResponseEntity.created(new URI("/api/admin/users/" + newUser.getLogin())) + .headers(HeaderUtil.createAlert(applicationName, "userManagement.created", newUser.getLogin())) + .body(newUser); + } + } + + /** + * {@code PUT /admin/users} : Updates an existing User. + * + * @param userDTO the user to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated user. + * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already in use. + * @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already in use. + */ + @PutMapping({ "/users", "/users/{login}" }) + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity updateUser( + @PathVariable(name = "login", required = false) @Pattern(regexp = Constants.LOGIN_REGEX) String login, + @Valid @RequestBody AdminUserDTO userDTO + ) { + log.debug("REST request to update User : {}", userDTO); + Optional existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.orElseThrow().getId().equals(userDTO.getId()))) { + throw new EmailAlreadyUsedException(); + } + existingUser = userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()); + if (existingUser.isPresent() && (!existingUser.orElseThrow().getId().equals(userDTO.getId()))) { + throw new LoginAlreadyUsedException(); + } + Optional updatedUser = userService.updateUser(userDTO); + + return ResponseUtil.wrapOrNotFound( + updatedUser, + HeaderUtil.createAlert(applicationName, "userManagement.updated", userDTO.getLogin()) + ); + } + + /** + * {@code GET /admin/users} : get all users with all the details - calling this are only allowed for the administrators. + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users. + */ + @GetMapping("/users") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity> getAllUsers(@org.springdoc.core.annotations.ParameterObject Pageable pageable) { + log.debug("REST request to get all User for an admin"); + if (!onlyContainsAllowedProperties(pageable)) { + return ResponseEntity.badRequest().build(); + } + + final Page page = userService.getAllManagedUsers(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + private boolean onlyContainsAllowedProperties(Pageable pageable) { + return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains); + } + + /** + * {@code GET /admin/users/:login} : get the "login" user. + * + * @param login the login of the user to find. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the "login" user, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/users/{login}") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity getUser(@PathVariable("login") @Pattern(regexp = Constants.LOGIN_REGEX) String login) { + log.debug("REST request to get User : {}", login); + return ResponseUtil.wrapOrNotFound(userService.getUserWithAuthoritiesByLogin(login).map(AdminUserDTO::new)); + } + + /** + * {@code DELETE /admin/users/:login} : delete the "login" User. + * + * @param login the login of the user to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/users/{login}") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity deleteUser(@PathVariable("login") @Pattern(regexp = Constants.LOGIN_REGEX) String login) { + log.debug("REST request to delete User: {}", login); + userService.deleteUser(login); + return ResponseEntity.noContent().headers(HeaderUtil.createAlert(applicationName, "userManagement.deleted", login)).build(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/VariableCategoryResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/VariableCategoryResource.java new file mode 100644 index 0000000..fdaf6df --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/VariableCategoryResource.java @@ -0,0 +1,79 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.repository.VariableCategoryRepository; +import com.oguerreiro.resilient.service.VariableCategoryService; +import com.oguerreiro.resilient.service.dto.VariableCategoryDTO; +import com.oguerreiro.resilient.service.mapper.VariableCategoryMapper; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.VariableCategory}. + */ +@RestController +@RequestMapping("/api/variable-categories") +public class VariableCategoryResource extends AbstractResilientResource { + private static final String ENTITY_NAME = "variableCategory"; + + private final VariableCategoryService variableCategoryService; + + public VariableCategoryResource(VariableCategoryService variableCategoryService, + VariableCategoryRepository variableCategoryRepository, VariableCategoryMapper variableCategoryMapper) { + super(VariableCategory.class, variableCategoryRepository, variableCategoryService); + + this.variableCategoryService = variableCategoryService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + @Override + protected Sort getAllSort() { + return Sort.by(Sort.Order.asc("variableScope.code"), Sort.Order.asc("code")); + } + + /* VariableCategoryResource endpoints */ + /** + * {@code GET /variable-categories/byScope/{:scopeId} : get all the VariableCategory within the scopeId. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of units in body. + */ + @GetMapping("/byScope/{scopeId}") + public List getAllByScope(@PathVariable("scopeId") Long scopeId) { + log.debug("REST request to get all getAllByScope Id : {}", scopeId); + return variableCategoryService.findAllByScope(scopeId, true); + } + + /** + * {@code GET /variable-categories/byScope/forData/{:scopeId} : get all the VariableCategory. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of units in body. + */ + @GetMapping("/byScope/forData/{scopeId}") + public List getAllByScopeForData(@PathVariable("scopeId") Long scopeId) { + log.debug("REST request to get all getAllByScopeForData Id : {}", scopeId); + return variableCategoryService.findAllByScope(scopeId, false); + } + + /** + * {@code GET /variable-categories/byScope/forFactor/{:scopeId} : get all the VariableCategory. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of units in body. + */ + @GetMapping("/byScope/forFactor/{scopeId}") + public List getAllByScopeForFactor(@PathVariable("scopeId") Long scopeId) { + log.debug("REST request to get all getAllByScopeForFactor Id : {}", scopeId); + return variableCategoryService.findAllByScopeForFactor(scopeId); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/VariableClassResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/VariableClassResource.java new file mode 100644 index 0000000..12bf2cd --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/VariableClassResource.java @@ -0,0 +1,34 @@ +package com.oguerreiro.resilient.web.rest; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.VariableClass; +import com.oguerreiro.resilient.repository.VariableClassRepository; +import com.oguerreiro.resilient.service.VariableClassService; +import com.oguerreiro.resilient.service.dto.VariableClassDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.VariableClass}. + */ +@RestController +@RequestMapping("/api/variable-classes") +public class VariableClassResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "variableClass"; + + private final VariableClassService variableClassService; + + public VariableClassResource(VariableClassService variableClassService, + VariableClassRepository variableClassRepository) { + super(VariableClass.class, variableClassRepository, variableClassService); + this.variableClassService = variableClassService; + } + + /* AbstractResilientResource methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/VariableClassTypeResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/VariableClassTypeResource.java new file mode 100644 index 0000000..ac99a8e --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/VariableClassTypeResource.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.VariableClassType; +import com.oguerreiro.resilient.repository.VariableClassTypeRepository; +import com.oguerreiro.resilient.service.VariableClassService; +import com.oguerreiro.resilient.service.VariableClassTypeService; +import com.oguerreiro.resilient.service.dto.VariableClassDTO; +import com.oguerreiro.resilient.service.dto.VariableClassTypeDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.VariableClassType}. + */ +@RestController +@RequestMapping("/api/variable-class-types") +public class VariableClassTypeResource + extends AbstractResilientResource { + private static final String ENTITY_NAME = "variableClassType"; + + private final VariableClassService variableClassService; + + public VariableClassTypeResource(VariableClassTypeService variableClassTypeService, + VariableClassTypeRepository variableClassTypeRepository, VariableClassService variableClassService) { + super(VariableClassType.class, variableClassTypeRepository, variableClassTypeService); + this.variableClassService = variableClassService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* VariableClassTypeResource endpoints */ + @GetMapping("/classes/forVariable/{variableId}") + public List getAllForVariable(@PathVariable("variableId") Long variableId) { + log.debug("REST request to get all getAllForVariable Id : {}", variableId); + return variableClassService.getAllForVariable(variableId); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/VariableResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/VariableResource.java new file mode 100644 index 0000000..de522a2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/VariableResource.java @@ -0,0 +1,105 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Sort; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.repository.VariableRepository; +import com.oguerreiro.resilient.service.VariableService; +import com.oguerreiro.resilient.service.criteria.VariableCriteria; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.dto.VariableDTO; +import com.oguerreiro.resilient.service.query.VariableQueryService; + +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.Variable}. + */ +@RestController +@RequestMapping("/api/variables") +public class VariableResource extends AbstractResilientQueryResource { + + private final Logger log = LoggerFactory.getLogger(VariableResource.class); + + private static final String ENTITY_NAME = "variable"; + + private final VariableService variableService; + + private final VariableQueryService variableQueryService; + + public VariableResource(VariableService variableService, VariableRepository variableRepository, + VariableQueryService variableQueryService) { + super(Variable.class, variableRepository, variableService, variableQueryService); + this.variableService = variableService; + this.variableQueryService = variableQueryService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + @Override + protected Sort getAllSort() { + return Sort.by(Sort.Order.asc("code")); + } + + /* Custom VariableResource endpoints */ + /** + * {@code GET /variables/count} : count all the variables. + * + * @param criteria the criteria which the requested entities should match. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the count in body. + */ + @PreAuthorize("@resilientSecurity.canAccess('READ', 'com.oguerreiro.resilient.domain.Variable')") // Inventory, delegates permission to Variable + @GetMapping("/count") + public ResponseEntity countVariables(VariableCriteria criteria) { + log.debug("REST request to count Variables by criteria: {}", criteria); + return ResponseEntity.ok().body(variableQueryService.countByCriteria(criteria)); + } + + @PreAuthorize("@resilientSecurity.canAccess('READ', 'com.oguerreiro.resilient.domain.Variable')") // Inventory, delegates permission to Variable + @GetMapping("/{id}/baseUnit") + public ResponseEntity getVariableBaseUnit(@PathVariable("id") Long id) { + log.debug("REST request to getVariableBaseUnit : {}", id); + + return ResponseUtil.wrapOrNotFound(variableService.getVariableBaseUnit(id)); + } + + @PreAuthorize("@resilientSecurity.canAccess('READ', 'com.oguerreiro.resilient.domain.Variable')") // Inventory, delegates permission to Variable + @GetMapping("/forManualInput") + public ResponseEntity> getVariableforManualInput() { + log.debug("REST request to getVariableforManualInput"); + + List domainList = variableService.findVariableForManualInput(); + return ResponseEntity.ok().body(domainList); + } + + @GetMapping("/export") + public ResponseEntity exportVariables() { + log.debug("REST request to export Variables: {}"); + + byte[] fileContent = this.variableService.exportVariables(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDisposition(ContentDisposition.attachment().filename("report.xlsx").build()); + + return new ResponseEntity<>(fileContent, headers, HttpStatus.OK); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/VariableScopeResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/VariableScopeResource.java new file mode 100644 index 0000000..8112e33 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/VariableScopeResource.java @@ -0,0 +1,61 @@ +package com.oguerreiro.resilient.web.rest; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.repository.VariableScopeRepository; +import com.oguerreiro.resilient.service.VariableScopeService; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.VariableScope}. + */ +@RestController +@RequestMapping("/api/variable-scopes") +public class VariableScopeResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "variableScope"; + + private final VariableScopeService variableScopeService; + + public VariableScopeResource(VariableScopeService variableScopeService, + VariableScopeRepository variableScopeRepository) { + super(VariableScope.class, variableScopeRepository, variableScopeService); + + this.variableScopeService = variableScopeService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* VariableScopeResource endpoints */ + /** + * {@code GET /variable-scope/forData} : get all the VariableScope's with VariableScope.hiddenForData==FALSE + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of VariableScope's in body. + */ + @GetMapping("/forData") + public List getAllForData() { + log.debug("REST request to get all getAllForData Id : {}"); + return variableScopeService.findAllForData(); + } + + /** + * {@code GET /variable-scope/forFactor} : get all the VariableScope's with VariableScope.hiddenForFactor==FALSE + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of VariableScope's in body. + */ + @GetMapping("/forFactor") + public List getAllForFactor() { + log.debug("REST request to get all getAllForFactor Id : {}"); + return variableScopeService.findAllForFactor(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/VariableUnitsResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/VariableUnitsResource.java new file mode 100644 index 0000000..5a1324c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/VariableUnitsResource.java @@ -0,0 +1,172 @@ +package com.oguerreiro.resilient.web.rest; + +import com.oguerreiro.resilient.repository.VariableUnitsRepository; +import com.oguerreiro.resilient.service.VariableUnitsService; +import com.oguerreiro.resilient.service.dto.VariableUnitsDTO; +import com.oguerreiro.resilient.web.rest.errors.BadRequestAlertException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.domain.VariableUnits}. + */ +@RestController +@RequestMapping("/api/variable-units") +public class VariableUnitsResource { + + private final Logger log = LoggerFactory.getLogger(VariableUnitsResource.class); + + private static final String ENTITY_NAME = "variableUnits"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final VariableUnitsService variableUnitsService; + + private final VariableUnitsRepository variableUnitsRepository; + + public VariableUnitsResource(VariableUnitsService variableUnitsService, VariableUnitsRepository variableUnitsRepository) { + this.variableUnitsService = variableUnitsService; + this.variableUnitsRepository = variableUnitsRepository; + } + + /** + * {@code POST /variable-units} : Create a new variableUnits. + * + * @param variableUnitsDTO the variableUnitsDTO to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new variableUnitsDTO, or with status {@code 400 (Bad Request)} if the variableUnits has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("") + public ResponseEntity createVariableUnits(@RequestBody VariableUnitsDTO variableUnitsDTO) throws URISyntaxException { + log.debug("REST request to save VariableUnits : {}", variableUnitsDTO); + if (variableUnitsDTO.getId() != null) { + throw new BadRequestAlertException("A new variableUnits cannot already have an ID", ENTITY_NAME, "idexists"); + } + variableUnitsDTO = variableUnitsService.save(variableUnitsDTO); + return ResponseEntity.created(new URI("/api/variable-units/" + variableUnitsDTO.getId())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, variableUnitsDTO.getId().toString())) + .body(variableUnitsDTO); + } + + /** + * {@code PUT /variable-units/:id} : Updates an existing variableUnits. + * + * @param id the id of the variableUnitsDTO to save. + * @param variableUnitsDTO the variableUnitsDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated variableUnitsDTO, + * or with status {@code 400 (Bad Request)} if the variableUnitsDTO is not valid, + * or with status {@code 500 (Internal Server Error)} if the variableUnitsDTO couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/{id}") + public ResponseEntity updateVariableUnits( + @PathVariable(value = "id", required = false) final Long id, + @RequestBody VariableUnitsDTO variableUnitsDTO + ) throws URISyntaxException { + log.debug("REST request to update VariableUnits : {}, {}", id, variableUnitsDTO); + if (variableUnitsDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, variableUnitsDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!variableUnitsRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + variableUnitsDTO = variableUnitsService.update(variableUnitsDTO); + return ResponseEntity.ok() + .headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, variableUnitsDTO.getId().toString())) + .body(variableUnitsDTO); + } + + /** + * {@code PATCH /variable-units/:id} : Partial updates given fields of an existing variableUnits, field will ignore if it is null + * + * @param id the id of the variableUnitsDTO to save. + * @param variableUnitsDTO the variableUnitsDTO to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated variableUnitsDTO, + * or with status {@code 400 (Bad Request)} if the variableUnitsDTO is not valid, + * or with status {@code 404 (Not Found)} if the variableUnitsDTO is not found, + * or with status {@code 500 (Internal Server Error)} if the variableUnitsDTO couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/{id}", consumes = { "application/json", "application/merge-patch+json" }) + public ResponseEntity partialUpdateVariableUnits( + @PathVariable(value = "id", required = false) final Long id, + @RequestBody VariableUnitsDTO variableUnitsDTO + ) throws URISyntaxException { + log.debug("REST request to partial update VariableUnits partially : {}, {}", id, variableUnitsDTO); + if (variableUnitsDTO.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, variableUnitsDTO.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!variableUnitsRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = variableUnitsService.partialUpdate(variableUnitsDTO); + + return ResponseUtil.wrapOrNotFound( + result, + HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, variableUnitsDTO.getId().toString()) + ); + } + + /** + * {@code GET /variable-units} : get all the variableUnits. + * + * @param eagerload flag to eager load entities from relationships (This is applicable for many-to-many). + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of variableUnits in body. + */ + @GetMapping("") + public List getAllVariableUnits( + @RequestParam(name = "eagerload", required = false, defaultValue = "true") boolean eagerload + ) { + log.debug("REST request to get all VariableUnits"); + return variableUnitsService.findAll(); + } + + /** + * {@code GET /variable-units/:id} : get the "id" variableUnits. + * + * @param id the id of the variableUnitsDTO to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the variableUnitsDTO, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/{id}") + public ResponseEntity getVariableUnits(@PathVariable("id") Long id) { + log.debug("REST request to get VariableUnits : {}", id); + Optional variableUnitsDTO = variableUnitsService.findOne(id); + return ResponseUtil.wrapOrNotFound(variableUnitsDTO); + } + + /** + * {@code DELETE /variable-units/:id} : delete the "id" variableUnits. + * + * @param id the id of the variableUnitsDTO to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteVariableUnits(@PathVariable("id") Long id) { + log.debug("REST request to delete VariableUnits : {}", id); + variableUnitsService.delete(id); + return ResponseEntity.noContent() + .headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString())) + .build(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/BadRequestAlertException.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/BadRequestAlertException.java new file mode 100644 index 0000000..ea2bd27 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/BadRequestAlertException.java @@ -0,0 +1,49 @@ +package com.oguerreiro.resilient.web.rest.errors; + +import java.net.URI; +import org.springframework.http.HttpStatus; +import org.springframework.web.ErrorResponseException; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +public class BadRequestAlertException extends ErrorResponseException { + + private static final long serialVersionUID = 1L; + + private final String entityName; + + private final String errorKey; + + public BadRequestAlertException(String defaultMessage, String entityName, String errorKey) { + this(ErrorConstants.DEFAULT_TYPE, defaultMessage, entityName, errorKey); + } + + public BadRequestAlertException(URI type, String defaultMessage, String entityName, String errorKey) { + super( + HttpStatus.BAD_REQUEST, + ProblemDetailWithCauseBuilder.instance() + .withStatus(HttpStatus.BAD_REQUEST.value()) + .withType(type) + .withTitle(defaultMessage) + .withProperty("message", "error." + errorKey) + .withProperty("params", entityName) + .build(), + null + ); + this.entityName = entityName; + this.errorKey = errorKey; + } + + public String getEntityName() { + return entityName; + } + + public String getErrorKey() { + return errorKey; + } + + public ProblemDetailWithCause getProblemDetailWithCause() { + return (ProblemDetailWithCause) this.getBody(); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/EmailAlreadyUsedException.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/EmailAlreadyUsedException.java new file mode 100644 index 0000000..ea57ea8 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/EmailAlreadyUsedException.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.web.rest.errors; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +public class EmailAlreadyUsedException extends BadRequestAlertException { + + private static final long serialVersionUID = 1L; + + public EmailAlreadyUsedException() { + super(ErrorConstants.EMAIL_ALREADY_USED_TYPE, "Email is already in use!", "userManagement", "emailexists"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/ErrorConstants.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/ErrorConstants.java new file mode 100644 index 0000000..c1ad480 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/ErrorConstants.java @@ -0,0 +1,17 @@ +package com.oguerreiro.resilient.web.rest.errors; + +import java.net.URI; + +public final class ErrorConstants { + + public static final String ERR_CONCURRENCY_FAILURE = "error.concurrencyFailure"; + public static final String ERR_VALIDATION = "error.validation"; + public static final String PROBLEM_BASE_URL = "https://www.jhipster.tech/problem"; + public static final URI DEFAULT_TYPE = URI.create(PROBLEM_BASE_URL + "/problem-with-message"); + public static final URI CONSTRAINT_VIOLATION_TYPE = URI.create(PROBLEM_BASE_URL + "/constraint-violation"); + public static final URI INVALID_PASSWORD_TYPE = URI.create(PROBLEM_BASE_URL + "/invalid-password"); + public static final URI EMAIL_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/email-already-used"); + public static final URI LOGIN_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/login-already-used"); + + private ErrorConstants() {} +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslator.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslator.java new file mode 100644 index 0000000..4d881bc --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslator.java @@ -0,0 +1,264 @@ +package com.oguerreiro.resilient.web.rest.errors; + +import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.lang.Nullable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.ErrorResponse; +import org.springframework.web.ErrorResponseException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; +import tech.jhipster.web.util.HeaderUtil; + +/** + * Controller advice to translate the server side exceptions to client-friendly json structures. + * The error response follows RFC7807 - Problem Details for HTTP APIs (https://tools.ietf.org/html/rfc7807). + */ +@ControllerAdvice +public class ExceptionTranslator extends ResponseEntityExceptionHandler { + + private static final String FIELD_ERRORS_KEY = "fieldErrors"; + private static final String MESSAGE_KEY = "message"; + private static final String PATH_KEY = "path"; + private static final boolean CASUAL_CHAIN_ENABLED = false; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final Environment env; + + public ExceptionTranslator(Environment env) { + this.env = env; + } + + @ExceptionHandler + public ResponseEntity handleAnyException(Throwable ex, NativeWebRequest request) { + ProblemDetailWithCause pdCause = wrapAndCustomizeProblem(ex, request); + return handleExceptionInternal((Exception) ex, pdCause, buildHeaders(ex), HttpStatusCode.valueOf(pdCause.getStatus()), request); + } + + @Nullable + @Override + protected ResponseEntity handleExceptionInternal( + Exception ex, + @Nullable Object body, + HttpHeaders headers, + HttpStatusCode statusCode, + WebRequest request + ) { + body = body == null ? wrapAndCustomizeProblem((Throwable) ex, (NativeWebRequest) request) : body; + return super.handleExceptionInternal(ex, body, headers, statusCode, request); + } + + protected ProblemDetailWithCause wrapAndCustomizeProblem(Throwable ex, NativeWebRequest request) { + return customizeProblem(getProblemDetailWithCause(ex), ex, request); + } + + private ProblemDetailWithCause getProblemDetailWithCause(Throwable ex) { + if ( + ex instanceof com.oguerreiro.resilient.service.UsernameAlreadyUsedException + ) return (ProblemDetailWithCause) new LoginAlreadyUsedException().getBody(); + if ( + ex instanceof com.oguerreiro.resilient.service.EmailAlreadyUsedException + ) return (ProblemDetailWithCause) new EmailAlreadyUsedException().getBody(); + if ( + ex instanceof com.oguerreiro.resilient.service.InvalidPasswordException + ) return (ProblemDetailWithCause) new InvalidPasswordException().getBody(); + + if ( + ex instanceof ErrorResponseException exp && exp.getBody() instanceof ProblemDetailWithCause problemDetailWithCause + ) return problemDetailWithCause; + return ProblemDetailWithCauseBuilder.instance().withStatus(toStatus(ex).value()).build(); + } + + protected ProblemDetailWithCause customizeProblem(ProblemDetailWithCause problem, Throwable err, NativeWebRequest request) { + if (problem.getStatus() <= 0) problem.setStatus(toStatus(err)); + + if (problem.getType() == null || problem.getType().equals(URI.create("about:blank"))) problem.setType(getMappedType(err)); + + // higher precedence to Custom/ResponseStatus types + String title = extractTitle(err, problem.getStatus()); + String problemTitle = problem.getTitle(); + if (problemTitle == null || !problemTitle.equals(title)) { + problem.setTitle(title); + } + + if (problem.getDetail() == null) { + // higher precedence to cause + problem.setDetail(getCustomizedErrorDetails(err)); + } + + Map problemProperties = problem.getProperties(); + if (problemProperties == null || !problemProperties.containsKey(MESSAGE_KEY)) problem.setProperty( + MESSAGE_KEY, + getMappedMessageKey(err) != null ? getMappedMessageKey(err) : "error.http." + problem.getStatus() + ); + + if (problemProperties == null || !problemProperties.containsKey(PATH_KEY)) problem.setProperty(PATH_KEY, getPathValue(request)); + + if ( + (err instanceof MethodArgumentNotValidException fieldException) && + (problemProperties == null || !problemProperties.containsKey(FIELD_ERRORS_KEY)) + ) problem.setProperty(FIELD_ERRORS_KEY, getFieldErrors(fieldException)); + + problem.setCause(buildCause(err.getCause(), request).orElse(null)); + + return problem; + } + + private String extractTitle(Throwable err, int statusCode) { + return getCustomizedTitle(err) != null ? getCustomizedTitle(err) : extractTitleForResponseStatus(err, statusCode); + } + + private List getFieldErrors(MethodArgumentNotValidException ex) { + return ex + .getBindingResult() + .getFieldErrors() + .stream() + .map( + f -> + new FieldErrorVM( + f.getObjectName().replaceFirst("DTO$", ""), + f.getField(), + StringUtils.isNotBlank(f.getDefaultMessage()) ? f.getDefaultMessage() : f.getCode() + ) + ) + .toList(); + } + + private String extractTitleForResponseStatus(Throwable err, int statusCode) { + ResponseStatus specialStatus = extractResponseStatus(err); + return specialStatus == null ? HttpStatus.valueOf(statusCode).getReasonPhrase() : specialStatus.reason(); + } + + private String extractURI(NativeWebRequest request) { + HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class); + return nativeRequest != null ? nativeRequest.getRequestURI() : StringUtils.EMPTY; + } + + private HttpStatus toStatus(final Throwable throwable) { + // Let the ErrorResponse take this responsibility + if (throwable instanceof ErrorResponse err) return HttpStatus.valueOf(err.getBody().getStatus()); + + return Optional.ofNullable(getMappedStatus(throwable)).orElse( + Optional.ofNullable(resolveResponseStatus(throwable)).map(ResponseStatus::value).orElse(HttpStatus.INTERNAL_SERVER_ERROR) + ); + } + + private ResponseStatus extractResponseStatus(final Throwable throwable) { + return Optional.ofNullable(resolveResponseStatus(throwable)).orElse(null); + } + + private ResponseStatus resolveResponseStatus(final Throwable type) { + final ResponseStatus candidate = findMergedAnnotation(type.getClass(), ResponseStatus.class); + return candidate == null && type.getCause() != null ? resolveResponseStatus(type.getCause()) : candidate; + } + + private URI getMappedType(Throwable err) { + if (err instanceof MethodArgumentNotValidException) return ErrorConstants.CONSTRAINT_VIOLATION_TYPE; + return ErrorConstants.DEFAULT_TYPE; + } + + private String getMappedMessageKey(Throwable err) { + if (err instanceof MethodArgumentNotValidException) { + return ErrorConstants.ERR_VALIDATION; + } else if (err instanceof ConcurrencyFailureException || err.getCause() instanceof ConcurrencyFailureException) { + return ErrorConstants.ERR_CONCURRENCY_FAILURE; + } + return null; + } + + private String getCustomizedTitle(Throwable err) { + if (err instanceof MethodArgumentNotValidException) return "Method argument not valid"; + return null; + } + + private String getCustomizedErrorDetails(Throwable err) { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { + if (err instanceof HttpMessageConversionException) return "Unable to convert http message"; + if (err instanceof DataAccessException) return "Failure during data access"; + if (containsPackageName(err.getMessage())) return "Unexpected runtime exception"; + } + return err.getCause() != null ? err.getCause().getMessage() : err.getMessage(); + } + + private HttpStatus getMappedStatus(Throwable err) { + // Where we disagree with Spring defaults + if (err instanceof AccessDeniedException) return HttpStatus.FORBIDDEN; + if (err instanceof ConcurrencyFailureException) return HttpStatus.CONFLICT; + if (err instanceof BadCredentialsException) return HttpStatus.UNAUTHORIZED; + return null; + } + + private URI getPathValue(NativeWebRequest request) { + if (request == null) return URI.create("about:blank"); + return URI.create(extractURI(request)); + } + + private HttpHeaders buildHeaders(Throwable err) { + return err instanceof BadRequestAlertException badRequestAlertException + ? HeaderUtil.createFailureAlert( + applicationName, + true, + badRequestAlertException.getEntityName(), + badRequestAlertException.getErrorKey(), + badRequestAlertException.getMessage() + ) + : null; + } + + public Optional buildCause(final Throwable throwable, NativeWebRequest request) { + if (throwable != null && isCasualChainEnabled()) { + return Optional.of(customizeProblem(getProblemDetailWithCause(throwable), throwable, request)); + } + return Optional.ofNullable(null); + } + + private boolean isCasualChainEnabled() { + // Customize as per the needs + return CASUAL_CHAIN_ENABLED; + } + + private boolean containsPackageName(String message) { + // This list is for sure not complete + return StringUtils.containsAny( + message, + "org.", + "java.", + "net.", + "jakarta.", + "javax.", + "com.", + "io.", + "de.", + "com.oguerreiro.resilient" + ); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/FieldErrorVM.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/FieldErrorVM.java new file mode 100644 index 0000000..710c541 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/FieldErrorVM.java @@ -0,0 +1,32 @@ +package com.oguerreiro.resilient.web.rest.errors; + +import java.io.Serializable; + +public class FieldErrorVM implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String objectName; + + private final String field; + + private final String message; + + public FieldErrorVM(String dto, String field, String message) { + this.objectName = dto; + this.field = field; + this.message = message; + } + + public String getObjectName() { + return objectName; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/InvalidPasswordException.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/InvalidPasswordException.java new file mode 100644 index 0000000..425c60c --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/InvalidPasswordException.java @@ -0,0 +1,23 @@ +package com.oguerreiro.resilient.web.rest.errors; + +import org.springframework.http.HttpStatus; +import org.springframework.web.ErrorResponseException; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +public class InvalidPasswordException extends ErrorResponseException { + + private static final long serialVersionUID = 1L; + + public InvalidPasswordException() { + super( + HttpStatus.BAD_REQUEST, + ProblemDetailWithCauseBuilder.instance() + .withStatus(HttpStatus.BAD_REQUEST.value()) + .withType(ErrorConstants.INVALID_PASSWORD_TYPE) + .withTitle("Incorrect password") + .build(), + null + ); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/LoginAlreadyUsedException.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/LoginAlreadyUsedException.java new file mode 100644 index 0000000..247b4d2 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/LoginAlreadyUsedException.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.web.rest.errors; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +public class LoginAlreadyUsedException extends BadRequestAlertException { + + private static final long serialVersionUID = 1L; + + public LoginAlreadyUsedException() { + super(ErrorConstants.LOGIN_ALREADY_USED_TYPE, "Login name already used!", "userManagement", "userexists"); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/errors/package-info.java b/src/main/java/com/oguerreiro/resilient/web/rest/errors/package-info.java new file mode 100644 index 0000000..fd1b107 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/errors/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer error handling. + */ +package com.oguerreiro.resilient.web.rest.errors; diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/package-info.java b/src/main/java/com/oguerreiro/resilient/web/rest/package-info.java new file mode 100644 index 0000000..bf321ee --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer. + */ +package com.oguerreiro.resilient.web.rest; diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/security/SecurityGroupResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/security/SecurityGroupResource.java new file mode 100644 index 0000000..6e427c7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/security/SecurityGroupResource.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.web.rest.security; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.repository.security.SecurityGroupRepository; +import com.oguerreiro.resilient.security.resillient.SecurityGroup; +import com.oguerreiro.resilient.service.dto.security.SecurityGroupDTO; +import com.oguerreiro.resilient.service.dto.security.SecurityGroupPermissionDTO; +import com.oguerreiro.resilient.service.security.SecurityGroupService; +import com.oguerreiro.resilient.web.rest.AbstractResilientResource; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.security.SecurityGroup}. + */ +@RestController +@RequestMapping("/api/security/groups") +public class SecurityGroupResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "SecurityGroup"; + private final SecurityGroupService securityGroupService; + + public SecurityGroupResource(SecurityGroupRepository securityGroupRepository, + SecurityGroupService securityGroupService) { + super(SecurityGroup.class, securityGroupRepository, securityGroupService); + + this.securityGroupService = securityGroupService; + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* DashboardResource endpoints */ + @GetMapping("/{id}/permissions") + public List getSecurityGroupPermissions(@PathVariable("id") Long securityGroupid) { + log.debug("REST request to getSecurityGroupPermissions " + this.getClass().getSimpleName() + "'s."); + return securityGroupService.findPermissionsFor(securityGroupid); + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/security/SecurityResourceResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/security/SecurityResourceResource.java new file mode 100644 index 0000000..b274870 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/security/SecurityResourceResource.java @@ -0,0 +1,34 @@ +package com.oguerreiro.resilient.web.rest.security; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.oguerreiro.resilient.repository.security.SecurityResourceRepository; +import com.oguerreiro.resilient.security.resillient.SecurityResource; +import com.oguerreiro.resilient.service.dto.security.SecurityResourceDTO; +import com.oguerreiro.resilient.service.security.SecurityResourceService; +import com.oguerreiro.resilient.web.rest.AbstractResilientResource; + +/** + * REST controller for managing {@link com.oguerreiro.resilient.security.SecurityResource}. + */ +@RestController +@RequestMapping("/api/security/resources") +public class SecurityResourceResource extends AbstractResilientResource { + + private static final String ENTITY_NAME = "SecurityResource"; + + public SecurityResourceResource(SecurityResourceRepository securityResourceRepository, + SecurityResourceService securityResourceService) { + super(SecurityResource.class, securityResourceRepository, securityResourceService); + } + + /* Abstract methods impl*/ + @Override + protected String getEntityName() { + return ENTITY_NAME; + } + + /* DashboardResource endpoints */ + +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/vm/KeyAndPasswordVM.java b/src/main/java/com/oguerreiro/resilient/web/rest/vm/KeyAndPasswordVM.java new file mode 100644 index 0000000..1922f75 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/vm/KeyAndPasswordVM.java @@ -0,0 +1,27 @@ +package com.oguerreiro.resilient.web.rest.vm; + +/** + * View Model object for storing the user's key and password. + */ +public class KeyAndPasswordVM { + + private String key; + + private String newPassword; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/vm/ManagedUserVM.java b/src/main/java/com/oguerreiro/resilient/web/rest/vm/ManagedUserVM.java new file mode 100644 index 0000000..ea3c369 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/vm/ManagedUserVM.java @@ -0,0 +1,35 @@ +package com.oguerreiro.resilient.web.rest.vm; + +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import jakarta.validation.constraints.Size; + +/** + * View Model extending the AdminUserDTO, which is meant to be used in the user management UI. + */ +public class ManagedUserVM extends AdminUserDTO { + + public static final int PASSWORD_MIN_LENGTH = 4; + + public static final int PASSWORD_MAX_LENGTH = 100; + + @Size(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH) + private String password; + + public ManagedUserVM() { + // Empty constructor needed for Jackson. + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + // prettier-ignore + @Override + public String toString() { + return "ManagedUserVM{" + super.toString() + "} "; + } +} diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/vm/package-info.java b/src/main/java/com/oguerreiro/resilient/web/rest/vm/package-info.java new file mode 100644 index 0000000..fe5ffe4 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/web/rest/vm/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer visual models. + */ +package com.oguerreiro.resilient.web.rest.vm; diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..cc0830b --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,11 @@ + + ${AnsiColor.GREEN}███████╗ ████████╗ ██████╗ ████████╗ ██╗ ████████╗ ████████╗ ███ ██╗ ████████╗ + ${AnsiColor.GREEN}██╔═══██╗ ██╔═════╝ ██╔════╝ ╚══██╔══╝ ██║ ╚══██╔══╝ ██╔═════╝ ████╗ ██║ ╚══██╔══╝ + ${AnsiColor.GREEN}███████╔╝ ██████╗ ╚█████╗ ██║ ██║ ██║ ██████╗ ██║██╗██║ ██║ + ${AnsiColor.GREEN}██╔══██║ ██╔═══╝ ╚═══██╗ ██║ ██║ ██║ ██╔═══╝ ██║╚████║ ██║ + ${AnsiColor.GREEN}██║ ╚██╗ ████████╗ ██████╔╝ ████████╗ ████████╗ ████████╗ ████████╗ ██║ ╚███║ ██║ + ${AnsiColor.GREEN}╚═╝ ╚═╝ ╚═══════╝ ╚═════╝ ╚═══════╝ ╚═══════╝ ╚═══════╝ ╚═══════╝ ╚═╝ ╚══╝ ╚═╝ + +${AnsiColor.BRIGHT_BLUE}:: Resilient Framework :: ${resilient.version} :: +:: developed by Orlando Guerreiro :: omguerreiro@gmail.com :: +351 933 419 967 :: +:: Running Spring Boot ${spring-boot.version} :: Startup profile(s) ${spring.profiles.active} ::${AnsiColor.DEFAULT} diff --git a/src/main/resources/banner2.txt b/src/main/resources/banner2.txt new file mode 100644 index 0000000..5be7dbe --- /dev/null +++ b/src/main/resources/banner2.txt @@ -0,0 +1,10 @@ + + ${AnsiColor.GREEN} ██╗${AnsiColor.RED} ██╗ ██╗ ████████╗ ███████╗ ██████╗ ████████╗ ████████╗ ███████╗ + ${AnsiColor.GREEN} ██║${AnsiColor.RED} ██║ ██║ ╚══██╔══╝ ██╔═══██╗ ██╔════╝ ╚══██╔══╝ ██╔═════╝ ██╔═══██╗ + ${AnsiColor.GREEN} ██║${AnsiColor.RED} ████████║ ██║ ███████╔╝ ╚█████╗ ██║ ██████╗ ███████╔╝ + ${AnsiColor.GREEN}██╗ ██║${AnsiColor.RED} ██╔═══██║ ██║ ██╔════╝ ╚═══██╗ ██║ ██╔═══╝ ██╔══██║ + ${AnsiColor.GREEN}╚██████╔╝${AnsiColor.RED} ██║ ██║ ████████╗ ██║ ██████╔╝ ██║ ████████╗ ██║ ╚██╗ + ${AnsiColor.GREEN} ╚═════╝ ${AnsiColor.RED} ╚═╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═╝ + +${AnsiColor.BRIGHT_BLUE}:: JHipster 🤓 :: Running Spring Boot ${spring-boot.version} :: Startup profile(s) ${spring.profiles.active} :: +:: https://www.jhipster.tech ::${AnsiColor.DEFAULT} diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000..91c2b32 --- /dev/null +++ b/src/main/resources/config/application-dev.yml @@ -0,0 +1,198 @@ +# =================================================================== +# Spring Boot configuration for the "dev" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +logging: + level: + ROOT: DEBUG + tech.jhipster: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.orm.jdbc.bind: TRACE + com.oguerreiro.resilient: DEBUG + org.opensaml: DEBUG + org.springframework.security.saml2: DEBUG + org.springframework.security: DEBUG + +spring: + jpa: + show-sql: true + devtools: + restart: + enabled: true + additional-exclude: static/** + livereload: + enabled: false # we use Webpack dev server + BrowserSync for livereload + jackson: + serialization: + indent-output: true + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:mysql://localhost:3306/resilient_resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true + username: root + password: root + # BROWNSEA Deploy + # url: jdbc:mariadb://192.168.1.99:3306/resilient_resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true + # username: root + # password: root%1234 + # REMEMBER to disable liquibase: "liquibase.enabled: false" + hikari: + poolName: Hikari + auto-commit: false + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + liquibase: + # Remove 'faker' if you do not want the sample data to be loaded automatically + # enabled: false + contexts: dev + mail: + host: localhost + port: 25 + username: + password: + messages: + cache-duration: PT1S # 1 second, see the ISO 8601 standard + thymeleaf: + cache: false + # security: # SAMLv2 Config Placeholder + +server: + port: 8081 + # port: 8443 + # ssl: + # key-store: classpath:keystore/keystore.p12 + # key-store-password: nova#123 + # key-store-type: PKCS12 + # key-alias: innova-ssl-cert + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 100 # Number of objects in each cache entry + # CORS is only enabled by default with the "dev" profile + cors: + # Allow Ionic for JHipster by default (* no longer allowed in Spring Boot 2.4+) + allowed-origins: 'http://innova.oguerreiro.com,https://innova.oguerreiro.com,https://resilient.localhost,http://resilient.localhost,http://localhost:8081,https://localhost:8081,http://localhost:8100,https://localhost:8100,http://localhost:9000,https://localhost:9000,http://localhost:4200,https://localhost:4200' + # Enable CORS when running in GitHub Codespaces + allowed-origin-patterns: 'https://*.githubpreview.dev' + allowed-methods: '*' + allowed-headers: '*' + exposed-headers: 'Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params' + allow-credentials: true + max-age: 1800 + security: + remember-me: + # security key (this key should be unique for your application, and kept secret) + key: 6436150a69ff50bcf383fbb9d974e2e7bd5c4439beaeef76e87a042d920db55f1f161147c30e01db9fd82117e47db521be8f + mail: # specific JHipster mail property, for standard properties see MailProperties + base-url: http://127.0.0.1:8081 + logging: + use-json-format: false # By default, logs are not in Json format + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + ring-buffer-size: 512 +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: + +# =================================================================== +# Resilient specific properties +# server.http +# enabled: SpringBoot application.yml dosen't allow both http and https, +# the solution is to provide http with a custom listener. +# Set this to 'true' to enable it. Defaults to 'false' +# port: The port number to listen. Defaults to 8081. +# mock-idp +# enabled: For DEV and test pourposes a mock-idp server is provided. +# Set this to 'true' to enable it on SpringBoot startup. Defaults to 'false' +# Available only with DEV profile. +# path: The path to idp.js file. Default impl is package in \src\main\mock-idp\idp.js +# =================================================================== + +resilient: + server: + http: + enabled: false + port: 8081 + mock-idp: + enabled: true + path: classpath:mock-idp/idp.js + security: + saml2: # ADDED to support SAMLv2 authentication to IDP. + # Metadata endpoint ${base-url}/saml2/service-provider-metadata/mock-idp + enabled: true + idp-id: unl-idp # The id of the IDP to use. One from the collection in relyingparty.registration + base-url: https://resilient.localhost # old: https://localhost:8443 + success-url: http://resilient.localhost/ + failure-url: http://resilient.localhost/login + relyingparty: + registration: + mock-idp: + assertingparty: + entity-id: http://mock-idp.localhost/saml/metadata # old: http://localhost:3000/saml/metadata + single-sign-on: + url: http://mock-idp.localhost/saml/sso # old: http://localhost:3000/saml/sso + # OPTIONAL. A list of query parameters to add to single-sign-on.url. This is usefull for mock-idp, to give instructions on how to behave + query-parameters: + spUrl: https://resilient.localhost # The callback to Service Provider, after IDP authentication (OK | KO). Appends the encoded url: acs=https%3A%2F%2Fresilient.localhost%2Flogin%2Fsaml2%2Fsso%2Fmock-idp + issuerUrl: http://mock-idp.localhost/saml/metadata # The IDP entity-id. This is needed for mock-idp to build saml2 response + single-logout: + url: http://mock-idp.localhost/saml/slo # old: http://localhost:3000/saml/slo + verification: + credentials: + - certificate-location: classpath:saml/idp-public.cert + want-authn-signed: false # Validate signature in entire message response (true-validates/false-doesn't validate) + want-assertion-signed: true # Validate signature in assertions message response (true-validates/false-doesn't validate) + signing: + credentials: + - private-key-location: classpath:saml/private.key + certificate-location: classpath:saml/public.cert + unl-idp: + assertingparty: + entity-id: http://unl-idp.localhost/saml/metadata + single-sign-on: + url: http://unl-idp.localhost/saml/sso + # OPTIONAL. A list of query parameters to add to single-sign-on.url. This is usefull for mock-idp, to give instructions on how to behave + query-parameters: + spUrl: https://resilient.localhost # The callback to Service Provider, after IDP authentication (OK | KO). Appends the encoded url: acs=https%3A%2F%2Fresilient.localhost%2Flogin%2Fsaml2%2Fsso%2Fmock-idp + issuerUrl: http://unl-idp.localhost/saml/metadata # The IDP entity-id. This is needed for mock-idp to build saml2 response + single-logout: + url: http://unl-idp.localhost/saml/slo # old: http://localhost:3000/saml/slo + verification: + credentials: + - certificate-location: classpath:saml/idp-public.cert + want-authn-signed: false # Validate signature in entire message response (true-validates/false-doesn't validate) + want-assertion-signed: true # Validate signature in assertions message response (true-validates/false-doesn't validate) + signing: + credentials: + - private-key-location: classpath:saml/private.key + certificate-location: classpath:saml/public.cert \ No newline at end of file diff --git a/src/main/resources/config/application-dev.yml.security.saml2.backup b/src/main/resources/config/application-dev.yml.security.saml2.backup new file mode 100644 index 0000000..1900c2c --- /dev/null +++ b/src/main/resources/config/application-dev.yml.security.saml2.backup @@ -0,0 +1,56 @@ +# =================================================================== +# This was my first implementation of SAML2. Using standard application.yml +# This was a SUCCESS. Then, I change it to be more dynamic +# Keep this has a reference +# =================================================================== + +spring: + security: + saml2: # ADDED to support SAMLv2 authentication to IDP. + relyingparty: + registration: + mock-idp: + assertingparty: + entity-id: http://localhost:3000/saml/metadata + single-sign-on: + url: http://localhost:3000/saml/sso + single-logout: + url: http://localhost:3000/saml/slo + verification: + credentials: + - certificate-location: classpath:saml/idp-public.cert + want-authn-signed: false # Validate signature in entire message response (true-validates/false-doesn't validate) + want-assertion-signed: true # Validate signature in assertions message response (true-validates/false-doesn't validate) + signing: + credentials: + - private-key-location: classpath:saml/private.key + certificate-location: classpath:saml/public.cert + +# =================================================================== +# This is the NEW WAY +# Custom Resilient configuration, that is loaded by saml2RelyingPartyRegistrationRepository.class +# and then, registered and configured in SecurityFilterChain +# =================================================================== +resilient: + security: + saml2: # ADDED to support SAMLv2 authentication to IDP. + enabled: true + relyingparty: + registration: + mock-idp: + assertingparty: + entity-id: http://localhost:3000/saml/metadata + single-sign-on: + url: http://localhost:3000/saml/sso + single-logout: + url: http://localhost:3000/saml/slo + verification: + credentials: + - certificate-location: classpath:saml/idp-public.cert + want-authn-signed: false # Validate signature in entire message response (true-validates/false-doesn't validate) + want-assertion-signed: true # Validate signature in assertions message response (true-validates/false-doesn't validate) + signing: + credentials: + - private-key-location: classpath:saml/private.key + certificate-location: classpath:saml/public.cert + \ No newline at end of file diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000..3695f21 --- /dev/null +++ b/src/main/resources/config/application-prod.yml @@ -0,0 +1,121 @@ +# =================================================================== +# Spring Boot configuration for the "prod" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +logging: + level: + ROOT: INFO + tech.jhipster: INFO + com.oguerreiro.resilient: INFO + +management: + prometheus: + metrics: + export: + enabled: false + +spring: + devtools: + restart: + enabled: false + livereload: + enabled: false + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:mysql://localhost:3306/resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true + username: root + password: + hikari: + poolName: Hikari + auto-commit: false + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + # Replace by 'prod, faker' to add the faker context and have sample data loaded in production + liquibase: + contexts: prod + mail: + host: localhost + port: 25 + username: + password: + thymeleaf: + cache: true + +# =================================================================== +# To enable TLS in production, generate a certificate using: +# keytool -genkey -alias resilient -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650 +# +# You can also use Let's Encrypt: +# See details in topic "Create a Java Keystore (.JKS) from Let's Encrypt Certificates" on https://maximilian-boehm.com/en-gb/blog +# +# Then, modify the server.ssl properties so your "server" configuration looks like: +# +# server: +# port: 443 +# ssl: +# key-store: classpath:config/tls/keystore.p12 +# key-store-password: password +# key-store-type: PKCS12 +# key-alias: selfsigned +# # The ciphers suite enforce the security by deactivating some old and deprecated SSL cipher, this list was tested against SSL Labs (https://www.ssllabs.com/ssltest/) +# ciphers: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 ,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA,TLS_RSA_WITH_CAMELLIA_128_CBC_SHA +# =================================================================== +server: + port: 8081 + shutdown: graceful # see https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-graceful-shutdown + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css,application/javascript,application/json,image/svg+xml + min-response-size: 1024 + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + http: + cache: # Used by the CachingHttpHeadersFilter + timeToLiveInDays: 1461 + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 1000 # Number of objects in each cache entry + security: + remember-me: + # security key (this key should be unique for your application, and kept secret) + key: 6436150a69ff50bcf383fbb9d974e2e7bd5c4439beaeef76e87a042d920db55f1f161147c30e01db9fd82117e47db521be8f + mail: # specific JHipster mail property, for standard properties see MailProperties + base-url: http://my-server-url-to-change # Modify according to your server's URL + logging: + use-json-format: false # By default, logs are not in Json format + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + ring-buffer-size: 512 +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: diff --git a/src/main/resources/config/application-tls.yml b/src/main/resources/config/application-tls.yml new file mode 100644 index 0000000..039f6f4 --- /dev/null +++ b/src/main/resources/config/application-tls.yml @@ -0,0 +1,19 @@ +# =================================================================== +# Activate this profile to enable TLS and HTTP/2. +# +# JHipster has generated a self-signed certificate, which will be used to encrypt traffic. +# As your browser will not understand this certificate, you will need to import it. +# +# Another (easiest) solution with Chrome is to enable the "allow-insecure-localhost" flag +# at chrome://flags/#allow-insecure-localhost +# =================================================================== +server: + ssl: + key-store: classpath:config/tls/keystore.p12 + key-store-password: password + key-store-type: PKCS12 + key-alias: selfsigned + ciphers: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + enabled-protocols: TLSv1.2 + http2: + enabled: true diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml new file mode 100644 index 0000000..b074247 --- /dev/null +++ b/src/main/resources/config/application.yml @@ -0,0 +1,219 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration will be overridden by the Spring profile you use, +# for example application-dev.yml if you use the "dev" profile. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +--- +# Conditionally disable springdoc on missing api-docs profile +spring: + config: + activate: + on-profile: '!api-docs' +springdoc: + api-docs: + enabled: false +--- +resilient: + version: "@project.version@" +management: + endpoints: + web: + base-path: /management + exposure: + include: + - configprops + - env + - health + - info + - jhimetrics + - jhiopenapigroups + - logfile + - loggers + - prometheus + - threaddump + - caches + - liquibase + endpoint: + health: + show-details: when_authorized + roles: 'ROLE_ADMIN' + probes: + enabled: true + group: + liveness: + include: livenessState + readiness: + include: readinessState,db + jhimetrics: + enabled: true + info: + git: + mode: full + env: + enabled: true + health: + mail: + enabled: false # When using the MailService, configure an SMTP server and set this to true + prometheus: + metrics: + export: + enabled: true + step: 60 + observations: + key-values: + application: ${spring.application.name} + metrics: + enable: + http: true + jvm: true + logback: true + process: true + system: true + distribution: + percentiles-histogram: + all: true + percentiles: + all: 0, 0.5, 0.75, 0.95, 0.99, 1.0 + data: + repository: + autotime: + enabled: true + +spring: + application: + name: resilient + profiles: + # The commented value for `active` can be replaced with valid Spring profiles to load. + # Otherwise, it will be filled in by maven when building the JAR file + # Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS` + active: '@spring.profiles.active@' + group: + dev: + - dev + - api-docs + # Uncomment to activate TLS for the dev profile + #- tls + jmx: + enabled: false + data: + jpa: + repositories: + bootstrap-mode: deferred + jpa: + open-in-view: false + properties: + hibernate.jdbc.time_zone: UTC + hibernate.timezone.default_storage: NORMALIZE + hibernate.type.preferred_instant_jdbc_type: TIMESTAMP + hibernate.id.new_generator_mappings: true + hibernate.connection.provider_disables_autocommit: true + hibernate.cache.use_second_level_cache: true + hibernate.cache.use_query_cache: false + hibernate.generate_statistics: false + # modify batch size as necessary + hibernate.jdbc.batch_size: 25 + hibernate.order_inserts: true + hibernate.order_updates: true + hibernate.query.fail_on_pagination_over_collection_fetch: true + hibernate.query.in_clause_parameter_padding: true + hibernate.javax.cache.missing_cache_strategy: create + hibernate: + ddl-auto: none + naming: + physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + messages: + basename: i18n/messages + fallback-to-system-locale: false # Added to force PT locale and NOT the system + main: + allow-bean-definition-overriding: true + mvc: + problemdetails: + enabled: true + task: + execution: + thread-name-prefix: resilient-task- + pool: + core-size: 2 + max-size: 50 + queue-capacity: 10000 + scheduling: + thread-name-prefix: resilient-scheduling- + pool: + size: 2 + thymeleaf: + mode: HTML + output: + ansi: + console-available: true + +server: + servlet: + session: + cookie: + http-only: true + +springdoc: + show-actuator: true + +# Properties to be exposed on the /info management endpoint +info: + # Comma separated list of profiles that will trigger the ribbon to show + display-ribbon-on-profiles: 'dev' + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + clientApp: + name: 'resilientApp' + # By default CORS is disabled. Uncomment to enable. + # cors: + # allowed-origins: "http://localhost:8100,http://localhost:9000" + # allowed-methods: "*" + # allowed-headers: "*" + # exposed-headers: "Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params" + # allow-credentials: true + # max-age: 1800 + mail: + from: resilient@localhost + api-docs: + default-include-pattern: /api/** + management-include-pattern: /management/** + title: Resilient API + description: Resilient API documentation + version: 0.0.1 + terms-of-service-url: + contact-name: + contact-url: + contact-email: + license: unlicensed + license-url: + security: + # content-security-policy: "default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:" + content-security-policy: "default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' https://fonts.googleapis.com/ 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com" +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: +locale: pt diff --git a/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml b/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml new file mode 100644 index 0000000..251e052 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091307_added_entity_OrganizationType.xml b/src/main/resources/config/liquibase/changelog/20241015091307_added_entity_OrganizationType.xml new file mode 100644 index 0000000..2a2faa9 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091307_added_entity_OrganizationType.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091307_added_entity_constraints_OrganizationType.xml b/src/main/resources/config/liquibase/changelog/20241015091307_added_entity_constraints_OrganizationType.xml new file mode 100644 index 0000000..60bc842 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091307_added_entity_constraints_OrganizationType.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091407_added_entity_Organization.xml b/src/main/resources/config/liquibase/changelog/20241015091407_added_entity_Organization.xml new file mode 100644 index 0000000..c8604ad --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091407_added_entity_Organization.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091407_added_entity_constraints_Organization.xml b/src/main/resources/config/liquibase/changelog/20241015091407_added_entity_constraints_Organization.xml new file mode 100644 index 0000000..9951f70 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091407_added_entity_constraints_Organization.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091507_added_entity_MetadataProperty.xml b/src/main/resources/config/liquibase/changelog/20241015091507_added_entity_MetadataProperty.xml new file mode 100644 index 0000000..f705945 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091507_added_entity_MetadataProperty.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091607_added_entity_MetadataValue.xml b/src/main/resources/config/liquibase/changelog/20241015091607_added_entity_MetadataValue.xml new file mode 100644 index 0000000..d79b9ff --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091607_added_entity_MetadataValue.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091707_added_entity_UnitType.xml b/src/main/resources/config/liquibase/changelog/20241015091707_added_entity_UnitType.xml new file mode 100644 index 0000000..3b839f1 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091707_added_entity_UnitType.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091807_added_entity_Unit.xml b/src/main/resources/config/liquibase/changelog/20241015091807_added_entity_Unit.xml new file mode 100644 index 0000000..1ce24fb --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091807_added_entity_Unit.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091807_added_entity_constraints_Unit.xml b/src/main/resources/config/liquibase/changelog/20241015091807_added_entity_constraints_Unit.xml new file mode 100644 index 0000000..12e6420 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091807_added_entity_constraints_Unit.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091907_added_entity_UnitConverter.xml b/src/main/resources/config/liquibase/changelog/20241015091907_added_entity_UnitConverter.xml new file mode 100644 index 0000000..1c1bd79 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091907_added_entity_UnitConverter.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015091907_added_entity_constraints_UnitConverter.xml b/src/main/resources/config/liquibase/changelog/20241015091907_added_entity_constraints_UnitConverter.xml new file mode 100644 index 0000000..863fe7e --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015091907_added_entity_constraints_UnitConverter.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092007_added_entity_VariableScope.xml b/src/main/resources/config/liquibase/changelog/20241015092007_added_entity_VariableScope.xml new file mode 100644 index 0000000..44cf45c --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092007_added_entity_VariableScope.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092107_added_entity_VariableCategory.xml b/src/main/resources/config/liquibase/changelog/20241015092107_added_entity_VariableCategory.xml new file mode 100644 index 0000000..507888f --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092107_added_entity_VariableCategory.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092107_added_entity_constraints_VariableCategory.xml b/src/main/resources/config/liquibase/changelog/20241015092107_added_entity_constraints_VariableCategory.xml new file mode 100644 index 0000000..3b22fbe --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092107_added_entity_constraints_VariableCategory.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092207_added_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20241015092207_added_entity_Variable.xml new file mode 100644 index 0000000..b140603 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092207_added_entity_Variable.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092207_added_entity_constraints_Variable.xml b/src/main/resources/config/liquibase/changelog/20241015092207_added_entity_constraints_Variable.xml new file mode 100644 index 0000000..4282903 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092207_added_entity_constraints_Variable.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092307_added_entity_VariableUnits.xml b/src/main/resources/config/liquibase/changelog/20241015092307_added_entity_VariableUnits.xml new file mode 100644 index 0000000..8cece1c --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092307_added_entity_VariableUnits.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092307_added_entity_constraints_VariableUnits.xml b/src/main/resources/config/liquibase/changelog/20241015092307_added_entity_constraints_VariableUnits.xml new file mode 100644 index 0000000..d7ee220 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092307_added_entity_constraints_VariableUnits.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092407_added_entity_VariableClass.xml b/src/main/resources/config/liquibase/changelog/20241015092407_added_entity_VariableClass.xml new file mode 100644 index 0000000..a5b7e63 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092407_added_entity_VariableClass.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092407_added_entity_constraints_VariableClass.xml b/src/main/resources/config/liquibase/changelog/20241015092407_added_entity_constraints_VariableClass.xml new file mode 100644 index 0000000..415c1bd --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092407_added_entity_constraints_VariableClass.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092507_added_entity_Period.xml b/src/main/resources/config/liquibase/changelog/20241015092507_added_entity_Period.xml new file mode 100644 index 0000000..9842ad4 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092507_added_entity_Period.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092607_added_entity_PeriodVersion.xml b/src/main/resources/config/liquibase/changelog/20241015092607_added_entity_PeriodVersion.xml new file mode 100644 index 0000000..27c8df0 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092607_added_entity_PeriodVersion.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092607_added_entity_constraints_PeriodVersion.xml b/src/main/resources/config/liquibase/changelog/20241015092607_added_entity_constraints_PeriodVersion.xml new file mode 100644 index 0000000..ada58c8 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092607_added_entity_constraints_PeriodVersion.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092707_added_entity_InputData.xml b/src/main/resources/config/liquibase/changelog/20241015092707_added_entity_InputData.xml new file mode 100644 index 0000000..f8c7d93 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092707_added_entity_InputData.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092707_added_entity_constraints_InputData.xml b/src/main/resources/config/liquibase/changelog/20241015092707_added_entity_constraints_InputData.xml new file mode 100644 index 0000000..a99b5a1 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092707_added_entity_constraints_InputData.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092807_added_entity_InputDataUpload.xml b/src/main/resources/config/liquibase/changelog/20241015092807_added_entity_InputDataUpload.xml new file mode 100644 index 0000000..80b1697 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092807_added_entity_InputDataUpload.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092807_added_entity_constraints_InputDataUpload.xml b/src/main/resources/config/liquibase/changelog/20241015092807_added_entity_constraints_InputDataUpload.xml new file mode 100644 index 0000000..cd7df67 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092807_added_entity_constraints_InputDataUpload.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092907_added_entity_InputDataUploadLog.xml b/src/main/resources/config/liquibase/changelog/20241015092907_added_entity_InputDataUploadLog.xml new file mode 100644 index 0000000..e75c89d --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092907_added_entity_InputDataUploadLog.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015092907_added_entity_constraints_InputDataUploadLog.xml b/src/main/resources/config/liquibase/changelog/20241015092907_added_entity_constraints_InputDataUploadLog.xml new file mode 100644 index 0000000..a80b990 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015092907_added_entity_constraints_InputDataUploadLog.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015093007_added_entity_OutputData.xml b/src/main/resources/config/liquibase/changelog/20241015093007_added_entity_OutputData.xml new file mode 100644 index 0000000..e8b2f04 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015093007_added_entity_OutputData.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015093007_added_entity_constraints_OutputData.xml b/src/main/resources/config/liquibase/changelog/20241015093007_added_entity_constraints_OutputData.xml new file mode 100644 index 0000000..59b3073 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015093007_added_entity_constraints_OutputData.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241015105339_import_standard_data.xml b/src/main/resources/config/liquibase/changelog/20241015105339_import_standard_data.xml new file mode 100644 index 0000000..c5b3c98 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241015105339_import_standard_data.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241024175307_added_entity_VariableClassType.xml b/src/main/resources/config/liquibase/changelog/20241024175307_added_entity_VariableClassType.xml new file mode 100644 index 0000000..dc4207e --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241024175307_added_entity_VariableClassType.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241025180907_altered_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20241025180907_altered_entity_Variable.xml new file mode 100644 index 0000000..95d15cf --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241025180907_altered_entity_Variable.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241025234007_change_parent_of_VariableClass.xml b/src/main/resources/config/liquibase/changelog/20241025234007_change_parent_of_VariableClass.xml new file mode 100644 index 0000000..1e933d5 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241025234007_change_parent_of_VariableClass.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241026005807_altered_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20241026005807_altered_entity_Variable.xml new file mode 100644 index 0000000..1062c56 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241026005807_altered_entity_Variable.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241027235307_altered_entity_VariableUnit.xml b/src/main/resources/config/liquibase/changelog/20241027235307_altered_entity_VariableUnit.xml new file mode 100644 index 0000000..48a23b1 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241027235307_altered_entity_VariableUnit.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241031074807_constraint_entity_VariableClass.xml b/src/main/resources/config/liquibase/changelog/20241031074807_constraint_entity_VariableClass.xml new file mode 100644 index 0000000..28766db --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241031074807_constraint_entity_VariableClass.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241101142307_altered_entity_InputData.xml b/src/main/resources/config/liquibase/changelog/20241101142307_altered_entity_InputData.xml new file mode 100644 index 0000000..0c59a66 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241101142307_altered_entity_InputData.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241110130700_altered_entity_InputData.xml b/src/main/resources/config/liquibase/changelog/20241110130700_altered_entity_InputData.xml new file mode 100644 index 0000000..90aa74d --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241110130700_altered_entity_InputData.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241110131000_altered_entity_InputDataUpload.xml b/src/main/resources/config/liquibase/changelog/20241110131000_altered_entity_InputDataUpload.xml new file mode 100644 index 0000000..8a5e8f1 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241110131000_altered_entity_InputDataUpload.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241119003525_added_entity_EmissionFactor.xml b/src/main/resources/config/liquibase/changelog/20241119003525_added_entity_EmissionFactor.xml new file mode 100644 index 0000000..089ccb9 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241119003525_added_entity_EmissionFactor.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241119003525_added_entity_constraints_EmissionFactor.xml b/src/main/resources/config/liquibase/changelog/20241119003525_added_entity_constraints_EmissionFactor.xml new file mode 100644 index 0000000..7f28f0b --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241119003525_added_entity_constraints_EmissionFactor.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241119230505_altered_entity_Unit.xml b/src/main/resources/config/liquibase/changelog/20241119230505_altered_entity_Unit.xml new file mode 100644 index 0000000..208666d --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241119230505_altered_entity_Unit.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241128171125_altered_entity_EmissionFactor.xml b/src/main/resources/config/liquibase/changelog/20241128171125_altered_entity_EmissionFactor.xml new file mode 100644 index 0000000..eb8918e --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241128171125_altered_entity_EmissionFactor.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241205125307_altered_entity_InputData.xml b/src/main/resources/config/liquibase/changelog/20241205125307_altered_entity_InputData.xml new file mode 100644 index 0000000..e0a8149 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241205125307_altered_entity_InputData.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241205232507_altered_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20241205232507_altered_entity_Variable.xml new file mode 100644 index 0000000..091e8ad --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241205232507_altered_entity_Variable.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20241206212207_altered_entity_OutputData.xml b/src/main/resources/config/liquibase/changelog/20241206212207_altered_entity_OutputData.xml new file mode 100644 index 0000000..a68d090 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241206212207_altered_entity_OutputData.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250119123000_altered_entity_InputData.xml b/src/main/resources/config/liquibase/changelog/20250119123000_altered_entity_InputData.xml new file mode 100644 index 0000000..3ff0167 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250119123000_altered_entity_InputData.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250119193000_altered_entity_InputDataUploadLog.xml b/src/main/resources/config/liquibase/changelog/20250119193000_altered_entity_InputDataUploadLog.xml new file mode 100644 index 0000000..7ac25df --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250119193000_altered_entity_InputDataUploadLog.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250119194000_altered_entity_UnitConverter.xml b/src/main/resources/config/liquibase/changelog/20250119194000_altered_entity_UnitConverter.xml new file mode 100644 index 0000000..771248c --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250119194000_altered_entity_UnitConverter.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250127183000_altered_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20250127183000_altered_entity_Variable.xml new file mode 100644 index 0000000..209cdcc --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250127183000_altered_entity_Variable.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250130133000_altered_entity_EmissionFactor.xml b/src/main/resources/config/liquibase/changelog/20250130133000_altered_entity_EmissionFactor.xml new file mode 100644 index 0000000..6be60d2 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250130133000_altered_entity_EmissionFactor.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250130152000_altered_entity_InputData.xml b/src/main/resources/config/liquibase/changelog/20250130152000_altered_entity_InputData.xml new file mode 100644 index 0000000..118b0d3 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250130152000_altered_entity_InputData.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250131120000_altered_entity_InputData.xml b/src/main/resources/config/liquibase/changelog/20250131120000_altered_entity_InputData.xml new file mode 100644 index 0000000..eb04653 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250131120000_altered_entity_InputData.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250131121000_altered_entity_OutputData.xml b/src/main/resources/config/liquibase/changelog/20250131121000_altered_entity_OutputData.xml new file mode 100644 index 0000000..352d318 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250131121000_altered_entity_OutputData.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250131122000_altered_entity_EmissionFactor.xml b/src/main/resources/config/liquibase/changelog/20250131122000_altered_entity_EmissionFactor.xml new file mode 100644 index 0000000..0ecc6e2 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250131122000_altered_entity_EmissionFactor.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250209225000_altered_entity_EmissionFactor.xml b/src/main/resources/config/liquibase/changelog/20250209225000_altered_entity_EmissionFactor.xml new file mode 100644 index 0000000..8c767ba --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250209225000_altered_entity_EmissionFactor.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250211161000_added_entity_DashoardComponent.xml b/src/main/resources/config/liquibase/changelog/20250211161000_added_entity_DashoardComponent.xml new file mode 100644 index 0000000..af95776 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250211161000_added_entity_DashoardComponent.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250211161001_added_entity_DashboardComponentDetail.xml b/src/main/resources/config/liquibase/changelog/20250211161001_added_entity_DashboardComponentDetail.xml new file mode 100644 index 0000000..ae0b564 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250211161001_added_entity_DashboardComponentDetail.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250215093000_altered_entity_DashboardComponentDetail.xml b/src/main/resources/config/liquibase/changelog/20250215093000_altered_entity_DashboardComponentDetail.xml new file mode 100644 index 0000000..b4525cb --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250215093000_altered_entity_DashboardComponentDetail.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250228164500_altered_entity_DashboardComponentDetail.xml b/src/main/resources/config/liquibase/changelog/20250228164500_altered_entity_DashboardComponentDetail.xml new file mode 100644 index 0000000..a69b6a1 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250228164500_altered_entity_DashboardComponentDetail.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + SET @row_number = 0; + UPDATE dashboard_component_detail + JOIN (SELECT id FROM dashboard_component_detail ORDER BY id) sorted + ON dashboard_component_detail.id = sorted.id + SET dashboard_component_detail.idx_order = (@row_number := @row_number + 1); + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250303185000_altered_entity_DashboardComponent.xml b/src/main/resources/config/liquibase/changelog/20250303185000_altered_entity_DashboardComponent.xml new file mode 100644 index 0000000..c61ceb6 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250303185000_altered_entity_DashboardComponent.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + UPDATE dashboard_component + SET dashboard_component.type = 'TABLE'; + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250306190000_altered_entity_DashboardComponentDetail.xml b/src/main/resources/config/liquibase/changelog/20250306190000_altered_entity_DashboardComponentDetail.xml new file mode 100644 index 0000000..9c64ff1 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250306190000_altered_entity_DashboardComponentDetail.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250318010000_altered_entity_DashboardComponent.xml b/src/main/resources/config/liquibase/changelog/20250318010000_altered_entity_DashboardComponent.xml new file mode 100644 index 0000000..346276c --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250318010000_altered_entity_DashboardComponent.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250318024500_altered_entity_DashboardComponentDetail.xml b/src/main/resources/config/liquibase/changelog/20250318024500_altered_entity_DashboardComponentDetail.xml new file mode 100644 index 0000000..434d708 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250318024500_altered_entity_DashboardComponentDetail.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250318024600_altered_entity_DashboardComponent.xml b/src/main/resources/config/liquibase/changelog/20250318024600_altered_entity_DashboardComponent.xml new file mode 100644 index 0000000..8562fcf --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250318024600_altered_entity_DashboardComponent.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250321121200_altered_entity_VariableCategory.xml b/src/main/resources/config/liquibase/changelog/20250321121200_altered_entity_VariableCategory.xml new file mode 100644 index 0000000..2a85434 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250321121200_altered_entity_VariableCategory.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250325000000_altered_entity_VariableScope.xml b/src/main/resources/config/liquibase/changelog/20250325000000_altered_entity_VariableScope.xml new file mode 100644 index 0000000..3d9bbfb --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250325000000_altered_entity_VariableScope.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250325000001_altered_entity_VariableCategory.xml b/src/main/resources/config/liquibase/changelog/20250325000001_altered_entity_VariableCategory.xml new file mode 100644 index 0000000..3b5e781 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250325000001_altered_entity_VariableCategory.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250328201500_altered_entity_DashboardComponentDetail.xml b/src/main/resources/config/liquibase/changelog/20250328201500_altered_entity_DashboardComponentDetail.xml new file mode 100644 index 0000000..9eb23b1 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250328201500_altered_entity_DashboardComponentDetail.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250331130000_altered_entity_Organization.xml b/src/main/resources/config/liquibase/changelog/20250331130000_altered_entity_Organization.xml new file mode 100644 index 0000000..f1a6fb4 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250331130000_altered_entity_Organization.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250331220000_altered_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20250331220000_altered_entity_Variable.xml new file mode 100644 index 0000000..30243eb --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250331220000_altered_entity_Variable.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250402144500_altered_entity_DashboardComponent.xml b/src/main/resources/config/liquibase/changelog/20250402144500_altered_entity_DashboardComponent.xml new file mode 100644 index 0000000..3d3efca --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250402144500_altered_entity_DashboardComponent.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250402200000_added_entity_Security.xml b/src/main/resources/config/liquibase/changelog/20250402200000_added_entity_Security.xml new file mode 100644 index 0000000..d797c03 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250402200000_added_entity_Security.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250409110000_altered_entity_SecurityResource.xml b/src/main/resources/config/liquibase/changelog/20250409110000_altered_entity_SecurityResource.xml new file mode 100644 index 0000000..115919c --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250409110000_altered_entity_SecurityResource.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250412010000_altered_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20250412010000_altered_entity_Variable.xml new file mode 100644 index 0000000..54cdd0b --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250412010000_altered_entity_Variable.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250414100000_added_entity_Document.xml b/src/main/resources/config/liquibase/changelog/20250414100000_added_entity_Document.xml new file mode 100644 index 0000000..c6a0aaa --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250414100000_added_entity_Document.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250415084000_altered_entity_DashboardComponent.xml b/src/main/resources/config/liquibase/changelog/20250415084000_altered_entity_DashboardComponent.xml new file mode 100644 index 0000000..141ef65 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250415084000_altered_entity_DashboardComponent.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250415163500_altered_entity_User.xml b/src/main/resources/config/liquibase/changelog/20250415163500_altered_entity_User.xml new file mode 100644 index 0000000..c349441 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250415163500_altered_entity_User.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250415200000_update_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20250415200000_update_entity_Variable.xml new file mode 100644 index 0000000..7a8ddb0 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250415200000_update_entity_Variable.xml @@ -0,0 +1,19 @@ + + + + + + + + output_single IS NULL + + + diff --git a/src/main/resources/config/liquibase/changelog/20250416090000_added_entity_ContentPage.xml b/src/main/resources/config/liquibase/changelog/20250416090000_added_entity_ContentPage.xml new file mode 100644 index 0000000..15f6508 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250416090000_added_entity_ContentPage.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250416120000_altered_entity_Variable.xml b/src/main/resources/config/liquibase/changelog/20250416120000_altered_entity_Variable.xml new file mode 100644 index 0000000..398c3ed --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250416120000_altered_entity_Variable.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250502222301_added_entity_DashboardComponentOrganization.xml b/src/main/resources/config/liquibase/changelog/20250502222301_added_entity_DashboardComponentOrganization.xml new file mode 100644 index 0000000..efa6975 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250502222301_added_entity_DashboardComponentOrganization.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20250506120000_altered_entity_ResilientLog.xml b/src/main/resources/config/liquibase/changelog/20250506120000_altered_entity_ResilientLog.xml new file mode 100644 index 0000000..761c737 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250506120000_altered_entity_ResilientLog.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20252104104000_added_entity_ResilientLog.xml b/src/main/resources/config/liquibase/changelog/20252104104000_added_entity_ResilientLog.xml new file mode 100644 index 0000000..901ca00 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20252104104000_added_entity_ResilientLog.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/data/authority.csv b/src/main/resources/config/liquibase/data/authority.csv new file mode 100644 index 0000000..af5c6df --- /dev/null +++ b/src/main/resources/config/liquibase/data/authority.csv @@ -0,0 +1,3 @@ +name +ROLE_ADMIN +ROLE_USER diff --git a/src/main/resources/config/liquibase/data/user.csv b/src/main/resources/config/liquibase/data/user.csv new file mode 100644 index 0000000..2577490 --- /dev/null +++ b/src/main/resources/config/liquibase/data/user.csv @@ -0,0 +1,3 @@ +id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by +1;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;pt-pt;system;system +2;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;pt-pt;system;system diff --git a/src/main/resources/config/liquibase/data/user_authority.csv b/src/main/resources/config/liquibase/data/user_authority.csv new file mode 100644 index 0000000..01dbdef --- /dev/null +++ b/src/main/resources/config/liquibase/data/user_authority.csv @@ -0,0 +1,4 @@ +user_id;authority_name +1;ROLE_ADMIN +1;ROLE_USER +2;ROLE_USER diff --git a/src/main/resources/config/liquibase/fake-data/blob/hipster.png b/src/main/resources/config/liquibase/fake-data/blob/hipster.png new file mode 100644 index 0000000..5b7d507 Binary files /dev/null and b/src/main/resources/config/liquibase/fake-data/blob/hipster.png differ diff --git a/src/main/resources/config/liquibase/fake-data/input_data.csv b/src/main/resources/config/liquibase/fake-data/input_data.csv new file mode 100644 index 0000000..7c892aa --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/input_data.csv @@ -0,0 +1,11 @@ +id;source_value;variable_value;imputed_value;source_type;data_date;change_date;change_username;data_source;data_user;data_comments;creation_date;creation_username;version +1;31790.86;5076.76;14182.61;AUTO;2024-10-14;2024-10-14T13:19:22;into hence;hm valiantly furiously;high-level;huzzah;2024-10-14T16:43:41;yum yet yahoo;497 +2;6639.1;11244.91;31245.28;MANUAL;2024-10-14;2024-10-14T14:10:37;mechanically since criteria;once curiously;joyously;deeply personal;2024-10-14T20:01:48;weight;19697 +3;14476.27;4988.12;22552.23;MANUAL;2024-10-14;2024-10-14T12:39:10;beneath frightened optimistically;wide brilliant;dreamily wherever;brand;2024-10-15T05:08:19;recant or aside;6233 +4;21517.23;22462.31;24020.75;AUTO;2024-10-15;2024-10-15T04:24:07;now;meanwhile;though hungrily;scented hmph;2024-10-14T11:15:31;angrily gee even;15803 +5;26396.47;13827.3;9348.26;MANUAL;2024-10-15;2024-10-14T23:57:26;armpit why;owlishly;shyly hmph uncork;lacquer;2024-10-14T21:13:26;faithful on;6337 +6;5149.3;3037.95;16620.4;MANUAL;2024-10-14;2024-10-14T16:47:39;whether;horror joyously;on shingle;loaf as distorted;2024-10-15T05:14:00;police almost;12166 +7;25871.71;19279.41;3338.88;FILE;2024-10-14;2024-10-14T22:46:01;blindfolded;push charset;jam-packed knottily next;idle enchanted titanium;2024-10-15T08:24:10;coliseum loathsome and;13182 +8;30611.47;31504.17;24575.53;AUTO;2024-10-14;2024-10-14T20:25:09;geez unlike;towards meanwhile system;thankfully meaty;quarrelsomely;2024-10-14T23:57:40;meaningfully fatherly;25380 +9;21035.94;1658.63;8435.49;MANUAL;2024-10-15;2024-10-14T13:52:50;beyond acidly;vastly complete;despite once;for the concerning;2024-10-15T03:04:02;whenever why;13909 +10;30685.28;18869.47;30593.5;DISTRIB;2024-10-15;2024-10-15T03:30:08;proctor tight waiting;ew by;tepid defiantly knowing;aboard blindside tender;2024-10-15T06:55:57;industrialize while oh;8510 diff --git a/src/main/resources/config/liquibase/fake-data/input_data_upload.csv b/src/main/resources/config/liquibase/fake-data/input_data_upload.csv new file mode 100644 index 0000000..31e8c97 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/input_data_upload.csv @@ -0,0 +1,11 @@ +id;title;data_file;data_file_content_type;upload_file_name;disk_file_name;type;state;comments;creation_date;creation_username;version +1;phooey;../fake-data/blob/hipster.png;image/png;although wingtip;bah physical;INVENTORY;PROCESSING;round delicious;2024-10-14T12:55:47;er pfft furthermore;26953 +2;balalaika clueless;../fake-data/blob/hipster.png;image/png;repeal;or kosher constraint;INVENTORY;UPLOADED;whoa;2024-10-14T18:07:51;gravitate what;18379 +3;lining;../fake-data/blob/hipster.png;image/png;ha gel closely;rightfully;INVENTORY;ERROR;warped indeed orange;2024-10-15T02:47:15;and insect;17176 +4;hmph;../fake-data/blob/hipster.png;image/png;sentiment;expertise;INVENTORY;PROCESSING;nor triangular;2024-10-15T07:30:44;remix;31925 +5;acidly unless like;../fake-data/blob/hipster.png;image/png;yuck;and homeland;INVENTORY;UPLOADED;reassuringly sweetly;2024-10-15T07:42:22;violent;2562 +6;guilty healthy;../fake-data/blob/hipster.png;image/png;jovially beyond;capital instantly;INVENTORY;UPLOADED;ew;2024-10-14T09:53:29;praise affect;14492 +7;severe victoriously;../fake-data/blob/hipster.png;image/png;weakly helo sadly;lest quirky vaccinate;ACCOUNTING;UPLOADED;for dreamily;2024-10-14T18:46:05;tummy anonymize;16 +8;united burden;../fake-data/blob/hipster.png;image/png;psst yet vice;kowtow;ACCOUNTING;PROCESSED;hesitate and;2024-10-15T00:04:37;really executive briskly;18139 +9;acrobatic around fluffy;../fake-data/blob/hipster.png;image/png;pfft than;though;INVENTORY;ERROR;audit well-informed slowly;2024-10-14T12:47:35;true;23531 +10;unlike to;../fake-data/blob/hipster.png;image/png;log jealously;partially meanwhile;ACCOUNTING;UPLOADED;off;2024-10-15T08:32:33;upon apostrophize cuss;23458 diff --git a/src/main/resources/config/liquibase/fake-data/input_data_upload_log.csv b/src/main/resources/config/liquibase/fake-data/input_data_upload_log.csv new file mode 100644 index 0000000..eacd961 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/input_data_upload_log.csv @@ -0,0 +1,11 @@ +id;log_message;creation_date;creation_username;version +1;flowery reskill;2024-10-15T06:51:41;correctly subsidiary asterisk;22716 +2;monthly;2024-10-14T23:38:36;because an that;25264 +3;consequently;2024-10-15T05:58:05;so;19443 +4;quizzically tidy;2024-10-15T05:26:47;sharply forked boohoo;27209 +5;amidst;2024-10-14T23:28:56;since;5636 +6;a;2024-10-15T02:01:09;ecstatic;14546 +7;eyeglasses barge;2024-10-14T17:29:42;ah;2288 +8;so;2024-10-15T08:18:25;truthfully ew tailgate;3850 +9;documentation tender;2024-10-15T07:00:01;puzzled;28442 +10;certainly liver;2024-10-14T23:00:19;pain avalanche;18355 diff --git a/src/main/resources/config/liquibase/fake-data/metadata_property.csv b/src/main/resources/config/liquibase/fake-data/metadata_property.csv new file mode 100644 index 0000000..9aa48ee --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/metadata_property.csv @@ -0,0 +1,11 @@ +id;code;name;description;mandatory;metadata_type;pattern;version +1;slump kindheartedly huzzah;worse even;below daily;true;STRING;drat;5213 +2;consequently pfft;anti burn-out;tell;false;DECIMAL;hospitalization;2219 +3;alongside;never well flashy;uh-huh;true;STRING;dead between or;16651 +4;toady;enthrone cozy searchingly;before;false;INTEGER;next;25710 +5;exhaust lest nor;grave;as cutlet;false;DATE;outfit qua than;20276 +6;penicillin;carp;a;false;BOOLEAN;girdle plus;17272 +7;grim;swiftly;as;false;INTEGER;aw kooky;24244 +8;sorrowful;snap vainly pesky;rightfully lament;true;BOOLEAN;however jol shrill;28673 +9;past properly;oh netball;sonnet hence;true;DATE;dance calibre;10971 +10;disbelieve tender;sidestream unto blah;lest;false;DECIMAL;shameless because vice;21621 diff --git a/src/main/resources/config/liquibase/fake-data/metadata_value.csv b/src/main/resources/config/liquibase/fake-data/metadata_value.csv new file mode 100644 index 0000000..155f3f2 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/metadata_value.csv @@ -0,0 +1,11 @@ +id;metadata_property_code;target_domain_key;target_domain_id;value;version +1;ick sick;deduct tomorrow;26723;garland gah;4975 +2;bath hinder above;opposite;25712;nursing optimistically;31794 +3;muscatel oily;underneath opposite watchful;21025;though since;22382 +4;to stark;instead;1994;monthly blah drat;15089 +5;around er kiddingly;boohoo once during;17854;sympathetic;17881 +6;inquisitively;huzzah since aid;27603;loop meanwhile ugh;13126 +7;wimp;consent;20151;mistrust query;11147 +8;greedy apparatus psst;drat battle drat;31233;given if frankly;15555 +9;adviser oh;hence;14124;rutabaga bustling bashfully;19597 +10;top well;never;11439;lazily boar colorful;32665 diff --git a/src/main/resources/config/liquibase/fake-data/organization.csv b/src/main/resources/config/liquibase/fake-data/organization.csv new file mode 100644 index 0000000..0900035 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/organization.csv @@ -0,0 +1,11 @@ +id;code;name;image;image_content_type;input_inventory;output_inventory;version +1;timely phew upset;unlike per up;../fake-data/blob/hipster.png;image/png;false;true;3951 +2;even when loudly;frantically;../fake-data/blob/hipster.png;image/png;false;true;14305 +3;abaft;tomorrow;../fake-data/blob/hipster.png;image/png;true;false;32011 +4;delightfully intently rightfully;ha following;../fake-data/blob/hipster.png;image/png;true;false;13157 +5;adorable or like;roasted tremendously grimace;../fake-data/blob/hipster.png;image/png;true;false;24281 +6;visitor;too;../fake-data/blob/hipster.png;image/png;false;false;13139 +7;spec beckon;pfft shamble yowza;../fake-data/blob/hipster.png;image/png;true;false;10802 +8;shoemaker;voluntarily gadzooks eulogise;../fake-data/blob/hipster.png;image/png;true;false;2858 +9;legalize;grimy noteworthy gah;../fake-data/blob/hipster.png;image/png;true;true;23570 +10;canal mourn;incomparable elude;../fake-data/blob/hipster.png;image/png;false;false;26614 diff --git a/src/main/resources/config/liquibase/fake-data/organization_type.csv b/src/main/resources/config/liquibase/fake-data/organization_type.csv new file mode 100644 index 0000000..519a538 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/organization_type.csv @@ -0,0 +1,11 @@ +id;code;name;description;nature;icon;icon_content_type;version +1;dwindle incidentally whoever;e-book;so hm apprehend;FACILITY;../fake-data/blob/hipster.png;image/png;31331 +2;harangue;aperitif reminisce;times clock;ORGANIZATION;../fake-data/blob/hipster.png;image/png;30883 +3;tenderly upright uh-huh;because tenderly;bumpy hyperventilate;ORGANIZATION;../fake-data/blob/hipster.png;image/png;30078 +4;via dutiful;considering;despite selection brief;PERSON;../fake-data/blob/hipster.png;image/png;8449 +5;this;athwart;ptarmigan;LEVEL;../fake-data/blob/hipster.png;image/png;30397 +6;enthusiastically stimulating sledge;humble mortified nor;as ugh wry;ORGANIZATION;../fake-data/blob/hipster.png;image/png;9486 +7;times why;vanquish vastly;repeatedly phooey near;PERSON;../fake-data/blob/hipster.png;image/png;18421 +8;surroundings;growing courageously;lest where;PERSON;../fake-data/blob/hipster.png;image/png;29466 +9;whoever fatherly sponsor;eek voluntarily enchanted;disrupt;ORGANIZATION;../fake-data/blob/hipster.png;image/png;6976 +10;pfft;brief;washtub oof truthfully;FACILITY;../fake-data/blob/hipster.png;image/png;23641 diff --git a/src/main/resources/config/liquibase/fake-data/output_data.csv b/src/main/resources/config/liquibase/fake-data/output_data.csv new file mode 100644 index 0000000..f19b99c --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/output_data.csv @@ -0,0 +1,11 @@ +id;value;version +1;24763.42;6363 +2;21279.34;2332 +3;20603.88;4168 +4;27980.64;14675 +5;22126.39;27251 +6;4786.49;31405 +7;2236.85;21055 +8;27018.04;23037 +9;12050.44;14416 +10;29200.92;25180 diff --git a/src/main/resources/config/liquibase/fake-data/period.csv b/src/main/resources/config/liquibase/fake-data/period.csv new file mode 100644 index 0000000..b0fc77d --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/period.csv @@ -0,0 +1,11 @@ +id;name;description;begin_date;end_date;state;creation_date;creation_username;version +1;anger over cacao;immediate while however;2024-10-14;2024-10-14;OPEN;2024-10-15T00:33:06;blindly whenever;11409 +2;ripe;yak;2024-10-15;2024-10-14;OPEN;2024-10-14T09:45:49;simple;11124 +3;catalyst ski;psst promptly harm;2024-10-14;2024-10-14;OPEN;2024-10-14T13:27:05;wrongly;12466 +4;honest barring;beyond;2024-10-14;2024-10-14;OPEN;2024-10-14T09:29:55;ah;21130 +5;sad;aircraft drawer ha;2024-10-15;2024-10-14;CLOSED;2024-10-14T23:29:05;times while;25819 +6;memorable meh paltry;apropos intervene;2024-10-14;2024-10-14;REOPEN;2024-10-15T06:23:16;moral;14166 +7;phew bah;boohoo;2024-10-15;2024-10-14;OPEN;2024-10-14T14:28:05;guerrilla;31753 +8;boo;giraffe;2024-10-15;2024-10-14;CLOSED;2024-10-15T04:36:20;spherical but minority;28461 +9;boohoo;natural;2024-10-15;2024-10-14;CLOSED;2024-10-15T07:09:01;because outlet vice;8733 +10;finally;tilt ceremony before;2024-10-14;2024-10-14;REOPEN;2024-10-14T14:49:42;whereas;26754 diff --git a/src/main/resources/config/liquibase/fake-data/period_version.csv b/src/main/resources/config/liquibase/fake-data/period_version.csv new file mode 100644 index 0000000..19a3e1e --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/period_version.csv @@ -0,0 +1,11 @@ +id;name;description;period_version;state;creation_date;creation_username;version +1;gestate;delightfully boohoo;12168;OPEN;2024-10-15T07:46:50;when skinny;18093 +2;currant phew;penalize canalise;3340;CLOSED;2024-10-14T15:36:57;opposite acrobatic loudly;27405 +3;abhor;above than anenst;32547;CLOSED;2024-10-14T18:03:55;furthermore uselessly than;9011 +4;pace impeccable;unique;9222;OPEN;2024-10-15T02:09:58;an irritably form;10812 +5;profess woot;oxygen before;27109;REOPEN;2024-10-14T21:50:48;unnecessarily ouch contrary;19973 +6;geld where downsize;recent kit;5403;CLOSED;2024-10-15T04:40:22;puff uh-huh unlike;16473 +7;occurrence seriously zowie;whereas intensely buoyant;2690;CLOSED;2024-10-15T03:12:03;bright;28457 +8;oh scratchy;men;8792;CLOSED;2024-10-14T12:26:13;shyly;9549 +9;ritualise forenenst on;issue for atop;1202;REOPEN;2024-10-14T20:48:00;handlebar;8805 +10;boohoo mask;inside equally;20288;OPEN;2024-10-14T17:54:04;pluck;11156 diff --git a/src/main/resources/config/liquibase/fake-data/unit.csv b/src/main/resources/config/liquibase/fake-data/unit.csv new file mode 100644 index 0000000..a48df20 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/unit.csv @@ -0,0 +1,11 @@ +id;code;symbol;name;description;convertion_rate;version +1;junk aha thin;instead;hence;yippee frail dirty;4912.19;25134 +2;toward colossal;urgently;what;considering;17496.39;1678 +3;why what though;consequently mmm;out overrate footwear;casualty;16343.78;5088 +4;aged lest tight;afore;since;sense unless;32755.1;7275 +5;gadzooks;fiber;psst fiercely pace;duh beyond but;18062.53;6769 +6;delightfully happily apud;yuck joyously;sow wick why;lost;26398.99;21006 +7;dearest;wild although;capital;real drat;17642.97;6456 +8;deputy high atop;yum;deceivingly slim or;silica greedily ready;24383.81;27566 +9;off strident real;silent reckless;supplement;opposite without sans;8158.79;7449 +10;indeed midst ick;broadly fully;afterwards collocate;drat;28805.79;31702 diff --git a/src/main/resources/config/liquibase/fake-data/unit_converter.csv b/src/main/resources/config/liquibase/fake-data/unit_converter.csv new file mode 100644 index 0000000..98d8381 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/unit_converter.csv @@ -0,0 +1,11 @@ +id;name;description;convertion_formula;version +1;unaccountably misdial;scratchy;eventually track viciously;6691 +2;yet tremble;transplant through;judgementally become even;14615 +3;righteously move goodie;yawningly;gadzooks;76 +4;phooey tomorrow before;though;inasmuch bah;25070 +5;inasmuch midst veldt;whinge trivial innovation;yowza tweet ew;26321 +6;mortally;gadzooks;in into lonely;25268 +7;upright ache;neat duh;overgraze after bah;22249 +8;whenever fitting mmm;cultivated where;aboard switch restfully;21918 +9;um;likely;whoever;20926 +10;boycott save reproachfully;vest a;innocent bah hmph;17325 diff --git a/src/main/resources/config/liquibase/fake-data/unit_type.csv b/src/main/resources/config/liquibase/fake-data/unit_type.csv new file mode 100644 index 0000000..7346f29 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/unit_type.csv @@ -0,0 +1,11 @@ +id;name;description;value_type;version +1;gee kiddingly fiercely;what expand meanwhile;BOOLEAN;15132 +2;avocado;drat but authentic;DECIMAL;18815 +3;boohoo;but yet affidavit;BOOLEAN;7783 +4;rigid expend dulcimer;beside lest;DECIMAL;24485 +5;cleave;er;DECIMAL;31506 +6;till how page;pfft;DECIMAL;32278 +7;along;parched upsell ferociously;DECIMAL;7893 +8;even around altruistic;enormously;DECIMAL;8254 +9;worldly jubilant at;icy searchingly since;DECIMAL;25731 +10;oof amongst accrue;ouch well-to-do;DECIMAL;21891 diff --git a/src/main/resources/config/liquibase/fake-data/variable.csv b/src/main/resources/config/liquibase/fake-data/variable.csv new file mode 100644 index 0000000..76e1498 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/variable.csv @@ -0,0 +1,9 @@ +id;code;variable_index;name;description;input;input_mode;output;output_formula;version +1;"79";7101;stepson;till drat;true;ANY;true;fake suddenly;28266 +3;"3850";12714;flower;furthermore;false;MANUAL;true;in trudge;28666 +5;"837";13782;oof;eek mostly;false;INTEGRATION;true;toward extreme mid;31168 +6;"70";20065;emotional;inexperienced before;false;DATAFILE;true;defame whereas;14215 +7;"61";428;concerned metallurgist;pish across meteorology;true;MANUAL;false;chuck leaver immediately;26149 +8;"7";24395;sparse phew;refusal pong;true;MANUAL;true;uh-huh;5210 +9;"6";25249;white anchored weakly;informal other pfft;true;DATAFILE;false;skimp next;26277 +10;"372";4325;entrance psst fiddle;cocktail plagiarize nest;true;ANY;false;scattering;5489 diff --git a/src/main/resources/config/liquibase/fake-data/variable_category.csv b/src/main/resources/config/liquibase/fake-data/variable_category.csv new file mode 100644 index 0000000..e9fa4e2 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/variable_category.csv @@ -0,0 +1,11 @@ +id;code;name;description;version +1;"89";boo consequently minty;ugh;9969 +2;"318";ha;cord highly;18791 +3;"55169";cruelly;finally jealously yum;5329 +4;"19554";whose adventurously continually;mmm about growling;20688 +5;"0";misty photoreceptor;selfish ouch drat;6044 +6;"06";inherit;ouch um;24792 +7;"4161";characterise whoever;messy;7234 +8;"82145";than lean soon;authentic;9480 +9;"810";now;awkwardly seriously;22252 +10;"7";ugh rash;automate furthermore;4815 diff --git a/src/main/resources/config/liquibase/fake-data/variable_class.csv b/src/main/resources/config/liquibase/fake-data/variable_class.csv new file mode 100644 index 0000000..4a7f879 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/variable_class.csv @@ -0,0 +1,11 @@ +id;code;name;version +1;geez;gaseous welcome;10602 +2;abaft boohoo;so;13744 +3;for;hungrily;5211 +4;deduction grizzle;broadly lest;11240 +5;but moult pungent;aha proof-reader lone;17489 +6;even immense but;repurpose if opposite;24340 +7;partially ah stark;ripe;10891 +8;will marketing trainer;which;9469 +9;consequently congressperson buttress;ha reproachfully insidious;28313 +10;amongst fooey;after;17349 diff --git a/src/main/resources/config/liquibase/fake-data/variable_scope.csv b/src/main/resources/config/liquibase/fake-data/variable_scope.csv new file mode 100644 index 0000000..a271a4e --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/variable_scope.csv @@ -0,0 +1,9 @@ +id;code;name;description;version +2;"90541";donate occasional aw;woot blindly ferryboat;1500 +3;"97";whoever into mutation;gah oof;24554 +4;"396";kiddingly provided for;boo knowledgeably er;16384 +5;"0";celsius psst accurate;guilty the shallow;4791 +6;"5";bah;enforcement nor monthly;27504 +7;"09";powerfully;outlet rehear since;27875 +8;"81";ouch;organic steep massive;21117 +10;"247";normalise nimble justly;stark;6690 diff --git a/src/main/resources/config/liquibase/fake-data/variable_units.csv b/src/main/resources/config/liquibase/fake-data/variable_units.csv new file mode 100644 index 0000000..f59068d --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/variable_units.csv @@ -0,0 +1,11 @@ +id;version +1;11090 +2;4644 +3;8874 +4;10087 +5;17843 +6;2374 +7;8031 +8;4945 +9;30670 +10;14751 diff --git a/src/main/resources/config/liquibase/fill-data/metadata_property.csv b/src/main/resources/config/liquibase/fill-data/metadata_property.csv new file mode 100644 index 0000000..0ec62cf --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/metadata_property.csv @@ -0,0 +1,3 @@ +id;code;name;description;mandatory;metadata_type;pattern +1;URL_SITE;Endereço website;Endereço do site [opcional];0;STRING; +2;ADDRESS;Endereço postal;Endereço das instalações;0;STRING; diff --git a/src/main/resources/config/liquibase/fill-data/organization.csv b/src/main/resources/config/liquibase/fill-data/organization.csv new file mode 100644 index 0000000..7b0c106 --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/organization.csv @@ -0,0 +1,12 @@ +id;parent_id;organization_type_id;code;name +1;;1;NOVA;Universidade NOVA (main org +2;1;2;UNL;Universidade Nova de Lisboa +3;1;2;FCT;Faculdade de Ciências e Tecnologia +4;1;2;FCSH;Faculdade de Ciências Sociais e Humanas +5;1;2;NSBE;Nova School of Business and Aconomics +6;1;2;FCM;Faculdade de Ciências Médicas +7;1;2;NSL;NOVA School of Law +8;1;2;IHMT;Instituto de Higiene e Medicina Tropical +9;1;2;NIMS;NOVA Information Management School +10;1;2;ITQBAX;Instituto de Tecnologia Química e Biologia António Xavier +11;1;2;ENSP;Escola Nacional de Saúde Pública diff --git a/src/main/resources/config/liquibase/fill-data/organization_type.csv b/src/main/resources/config/liquibase/fill-data/organization_type.csv new file mode 100644 index 0000000..e126cf5 --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/organization_type.csv @@ -0,0 +1,4 @@ +id;code;name;description;nature +1;MAIN;Topo da hierarquia;Topo da organização;ORGANIZATION +2;UO;Unidade Orgânica;Unidade Orgânica;ORGANIZATION +3;PERSON;Pessoa;Pessoa;PERSON diff --git a/src/main/resources/config/liquibase/fill-data/unit.csv b/src/main/resources/config/liquibase/fill-data/unit.csv new file mode 100644 index 0000000..16ca84a --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/unit.csv @@ -0,0 +1,39 @@ +id;unit_type_id;symbol;code;name;description;convertion_rate +1;1;nm;NANOMETRO;nanômetro;;100000 +2;1;µm;MICROMETRO;micrômetro;;10000 +3;1;mm;MILIMETRO;milímetro;;1000 +4;1;cm;CENTIMETRO;centímetro;;100 +5;1;dm;DECIMETRO;decímetro;;10 +6;1;m;METRO;metro;;1 +7;1;dam;DECAMETRO;decâmetro;;0,1 +8;1;hm;HECTOMETRO;hectômetro;;0,01 +9;1;km;QUILOMETRO;quilômetro;;0,001 +10;2;mg;MILIGRAMA;miligrama;;1000000 +11;2;cg;CENTIGRANA;centigrana;;100000 +12;2;dg;DECIGRAMA;decigrama;;10000 +13;2;g;GRAMA;grama;;1000 +14;2;dag;DECAGRAMA;decagrama;;100 +15;2;hg;HECTOGRAMA;hectograma;;10 +16;2;kg;QUILOGRAMA;quilograma;;1 +17;2;mag;MIRIAGRAMA;miriagrama;;0,1 +18;2;q;QUINTAL;quintal;;0,01 +19;2;t;TONELADA;tonelada;;0,001 +20;3;ml;MILISEGUNDO;milisegundo;;1000 +21;3;s;SEGUNDO;segundo;;1 +22;3;m;MINUTO;minuto;;0,016666667 +23;3;h;HORA;hora;;0,000277778 +24;4;mL;MILILITROS;mililitros;;1000 +25;4;cL;CENTILITROS;centilitros;;100 +26;4;dL;DECILITROS;decilitros;;10 +27;4;L;LITRO;litro;;1 +28;4;daL;DECALITROS;decalitros;;0,1 +29;4;hL;HECTOLITROS;hectolitros;;0,01 +30;4;kL;QUILOLITROS;quilolitros;;0,001 +31;5;mm³;MILIMETRO_CUBICO;milimetro cúbico;;1000 +32;5;cm³;CENTIMETRO_CUBICO;centimetro cúbico;;100 +33;5;dm³;DECIMETRO_CUBICO;decimetro cúbico;;10 +34;5;m³;METRO_CUBICO;metro cúbico;;1 +35;5;dam³;DECAMETRO_CUBICO;decametro cúbico;;0,1 +36;5;hm³;HECTOMETRO_CUBICO;hectometro cúbico;;0,01 +37;5;km³;QUILOMETRO_CUBICO;quilometro cúbico;;0,001 +38;6;sn;SIM_NAO;Sim/Não;;0 diff --git a/src/main/resources/config/liquibase/fill-data/unit_type.csv b/src/main/resources/config/liquibase/fill-data/unit_type.csv new file mode 100644 index 0000000..092d434 --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/unit_type.csv @@ -0,0 +1,7 @@ +id;name;description;value_type +1;Comprimento;O comprimento é a grandeza física que expressa a distância entre dois pontos.;DECIMAL +2;Massa;Massa é um conceito utilizado em ciências naturais. Em particular, a massa é frequentemente associada ao peso dos objetos;DECIMAL +3;Tempo;Tempo é a duração de fatos. É a maneira como contabilizamos os momentos, seja em horas, dias, semanas, séculos, etc.;DECIMAL +4;Liquido;Escala de medida de liquidos. Existe uma relação direta entre a medida de liquidos e volume.;DECIMAL +5;Volume;O volume é uma magnitude definida como o espaço ocupado por um corpo tridimensional. É uma função derivada, pois se acha multiplicando as três dimensões.;DECIMAL +6;Sim/Não;Tipo de dados simples, para recolha de respota binária: Sim | Não;BOOLEAN diff --git a/src/main/resources/config/liquibase/fill-data/var_category.csv b/src/main/resources/config/liquibase/fill-data/var_category.csv new file mode 100644 index 0000000..8a87db3 --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/var_category.csv @@ -0,0 +1,25 @@ +id;variable_scope_id;code;name ;description;AUX_IGNORE +1;5;00;Geral;Categoria Geral;5@00 +2;1;01;Combustão Fixa;Combustão Fixa;1@01 +3;1;02;Combustão Móvel;Combustão Móvel;1@02 +4;1;03;Gases Fluorados;Gases Fluorados;1@03 +5;1;04;Tratamento de Resíduos;Tratamento de Resíduos;1@04 +6;2;01;Geral 1;Categoria geral 1;2@01 +7;2;02;Geral 2;Categoria geral 2;2@02 +8;2;03;Geral 3;Categoria geral 3;2@03 +9;3;01;Bens e Serviços Adquiridos;Bens e Serviços Adquiridos (Categoria 1);3@01 +10;3;02;Bens de Capital;Bens de Capital (Categoria 2);3@02 +11;3;03;Actividades Relaccionadas com Energia e Combustivel;Actividades Relaccionadas com Energia e Combustivel (Categoria 3);3@03 +12;3;04;Transporte e Distribuição a Montante;Transporte e Distribuição a Montante (Categoria 4);3@04 +13;3;05;Resíduos Produzidos;Resíduos Produzidos (Categoria 5);3@05 +14;3;06;Deslocações em Serviço;Deslocações em Serviço (Categoria 6);3@06 +15;3;07;Deslocações Pendulares dos Trabalhadores;Deslocações Pendulares dos Trabalhadores (Categoria 7);3@07 +16;3;08;Ativos Arrendados a Montante;Ativos Arrendados a Montante (Categoria 8);3@08 +17;3;09;Transportes a Jusante;Transportes a Jusante (Categoria 9);3@09 +18;3;10;Processamento de produtos vendidos;Processamento de produtos vendidos (Categoria 10);3@10 +19;3;11;Uso de produtos vendidos;Uso de produtos vendidos (Categoria 11);3@11 +20;3;12;Tratamento de fim-de-vida dos produtos vendidos;Tratamento de fim-de-vida dos produtos vendidos (Categoria 12);3@12 +21;3;13;Ativos Arrendados a Jusante;Ativos Arrendados a Jusante (Categoria 13);3@13 +22;3;14;Franchises;Franchises (Categoria 14);3@14 +23;3;15;Investimentos;Investimentos (Categoria 15);3@15 +24;4;01;Geral;Categoria Geral;4@01 diff --git a/src/main/resources/config/liquibase/fill-data/var_scope.csv b/src/main/resources/config/liquibase/fill-data/var_scope.csv new file mode 100644 index 0000000..711edbb --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/var_scope.csv @@ -0,0 +1,6 @@ +id;code;name ;description +1;01;Âmbito 1;Variáveis GEE de Âmbito 1 +2;02;Âmbito 2;Variáveis GEE de Âmbito 2 +3;03;Âmbito 3;Variáveis GEE de Âmbito 3 +4;99;Outras Informações;Restante informação não inserivel nos outros âmbitos +5;00;Informação Geral;Âmbito geral de informação. Organização de variáveis não tipificadas GEE diff --git a/src/main/resources/config/liquibase/fill-data/variable.csv b/src/main/resources/config/liquibase/fill-data/variable.csv new file mode 100644 index 0000000..da449bc --- /dev/null +++ b/src/main/resources/config/liquibase/fill-data/variable.csv @@ -0,0 +1,119 @@ +id;variable_scope_id;variable_category_id;code;variable_index;name;description;input;output;input_mode;AUX_Scope_Code;AUX_Scope_Id;AUX_Category_Code;AUX_Category_Code_Key;AUX_Category_Id +1;1;2;010101;1;CONSUMOS DE GASÓLEO PARA COMBUSTÃO FIXA;CONSUMOS DE GASÓLEO PARA COMBUSTÃO FIXA;1;1;ANY;01;1;01;1@01;2 +2;1;2;010102;2;CONSUMOS DE GASOLINA PARA COMBUSTÃO FIXA;CONSUMOS DE GASOLINA PARA COMBUSTÃO FIXA;1;1;ANY;01;1;01;1@01;2 +3;1;2;010103;3;CONSUMOS DE GÁS NATURAL PARA COMBUSTÃO FIXA;CONSUMOS DE GÁS NATURAL PARA COMBUSTÃO FIXA;1;1;ANY;01;1;01;1@01;2 +4;1;2;010104;4;CONSUMOS DE PROPANO PARA COMBUSTÃO FIXA;CONSUMOS DE PROPANO PARA COMBUSTÃO FIXA;1;1;ANY;01;1;01;1@01;2 +5;1;2;010105;5;CONSUMOS DE BUTANO PARA COMBUSTÃO FIXA;CONSUMOS DE BUTANO PARA COMBUSTÃO FIXA;1;1;ANY;01;1;01;1@01;2 +6;1;2;010106;6;CONSUMOS DE GPL PARA COMBUSTÃO FIXA;CONSUMOS DE GPL PARA COMBUSTÃO FIXA;1;1;ANY;01;1;01;1@01;2 +7;1;2;010107;7;CONSUMOS DE BIOMASSA SÓLIDA / BIOCOMBUSTÍVEL;CONSUMOS DE BIOMASSA SÓLIDA / BIOCOMBUSTÍVEL;1;1;ANY;01;1;01;1@01;2 +8;1;2;010108;8;CONSUMOS DE BIOGÁS;CONSUMOS DE BIOGÁS;1;1;ANY;01;1;01;1@01;2 +9;1;3;010201;1;CONSUMOS DE GASÓLEO RODOVIÁRIO;CONSUMOS DE GASÓLEO RODOVIÁRIO;1;1;ANY;01;1;02;1@02;3 +10;1;3;010202;2;CONSUMOS DE GASOLINA RODOVIÁRIA;CONSUMOS DE GASOLINA RODOVIÁRIA;1;1;ANY;01;1;02;1@02;3 +11;1;3;010203;3;CONSUMOS DE GPL AUTO;CONSUMOS DE GPL AUTO;1;1;ANY;01;1;02;1@02;3 +12;1;3;010204;4;CONSUMOS DE GÁS NATURAL LIQUEFEITO RODOVIÁRIO;CONSUMOS DE GÁS NATURAL LIQUEFEITO RODOVIÁRIO;1;1;ANY;01;1;02;1@02;3 +13;1;3;010205;5;CONSUMOS DE GÁS NATURAL COMPRIMIDO RODOVIÁRIO;CONSUMOS DE GÁS NATURAL COMPRIMIDO RODOVIÁRIO;1;1;ANY;01;1;02;1@02;3 +14;1;3;010206;6;CONSUMOS DE BIOCOMBUSTÍVEL RODOVIÁRIO;CONSUMOS DE BIOCOMBUSTÍVEL RODOVIÁRIO;1;1;ANY;01;1;02;1@02;3 +15;1;4;010301;1;HFC-227EA - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO (Gases Fluorados);HFC-227EA - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO (Gases Fluorados);1;1;ANY;01;1;03;1@03;4 +16;1;4;010302;2;R-134A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;R-134A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +17;1;4;010303;3;R-23 - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;R-23 - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +18;1;4;010304;4;R-404A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;R-404A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +19;1;4;010305;5;R-422D - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;R-422D - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +20;1;4;010306;6;R-407C - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;R-407C - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +21;1;4;010307;7;SF6 - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;SF6 - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +22;1;4;010308;8;R-410A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;R-410A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +23;1;4;010309;9;R-417A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;R-417A - QUANTIDADE ADQUIRIDA NO DECORRER DO ANO;1;1;ANY;01;1;03;1@03;4 +24;1;5;010401;1;QUANTIDADE DE BIORESÍDUOS LEVADOS A COMPOSTAGEM;QUANTIDADE DE BIORESÍDUOS LEVADOS A COMPOSTAGEM;1;1;ANY;01;1;04;1@04;5 +25;2;6;020101;1;CONSUMOS DE ELETRICIDADE : FORNECEDOR A;CONSUMOS DE ELETRICIDADE : FORNECEDOR A;1;1;ANY;02;2;01;2@01;6 +26;2;6;020102;2;CONSUMOS DE ELETRICIDADE : FORNECEDOR B;CONSUMOS DE ELETRICIDADE : FORNECEDOR B;1;1;ANY;02;2;01;2@01;6 +27;2;6;020103;3;CONSUMOS DE ELETRICIDADE : FORNECEDOR C;CONSUMOS DE ELETRICIDADE : FORNECEDOR C;1;1;ANY;02;2;01;2@01;6 +28;2;6;020104;4;CONSUMOS DE ELETRICIDADE : FORNECEDOR D;CONSUMOS DE ELETRICIDADE : FORNECEDOR D;1;1;ANY;02;2;01;2@01;6 +29;2;7;020201;1;NÚMERO DE GARANTIAS DE ORIGEM DE ENERGIA RENOVÁVEL (GO) ANULADAS NO REGISTO OFICIAL;NÚMERO DE GARANTIAS DE ORIGEM DE ENERGIA RENOVÁVEL (GO) ANULADAS NO REGISTO OFICIAL;1;1;ANY;02;2;02;2@02;7 +30;2;7;020202;2;ELETRICIDADE ADQUIRIDA ATRAVÉS DE CONTRATOS BILATERAIS DE AQUISIÇÃO DE ENERGIA VERDE (100% RENOVÁVEL);ELETRICIDADE ADQUIRIDA ATRAVÉS DE CONTRATOS BILATERAIS DE AQUISIÇÃO DE ENERGIA VERDE (100% RENOVÁVEL);1;1;ANY;02;2;02;2@02;7 +31;2;8;020301;1;CONSUMOS DE ELETRICIDADE DE VEÍCULOS DA FROTA PRÓPRIA EM ABASTECIMENTO INTERNO;CONSUMOS DE ELETRICIDADE DE VEÍCULOS DA FROTA PRÓPRIA EM ABASTECIMENTO INTERNO;1;1;ANY;02;2;03;2@03;8 +32;2;8;020302;2;CONSUMOS DE ELETRICIDADE DE VEÍCULOS DA FROTA PRÓPRIA EM ABASTECIMENTO EXTERNO;CONSUMOS DE ELETRICIDADE DE VEÍCULOS DA FROTA PRÓPRIA EM ABASTECIMENTO EXTERNO;1;1;ANY;02;2;03;2@03;8 +33;3;9;030101;1;03000000-1 PRODUTOS DA AGRICULTURA, DA PESCA, DA SILVICULTURA E AFINS;03000000-1 PRODUTOS DA AGRICULTURA, DA PESCA, DA SILVICULTURA E AFINS;1;1;ANY;03;3;01;3@01;9 +34;3;9;030102;2;14000000-1 EXPLORAÇÃO MINEIRA, METAIS DE BASE E PRODUTOS AFINS;14000000-1 EXPLORAÇÃO MINEIRA, METAIS DE BASE E PRODUTOS AFINS;1;1;ANY;03;3;01;3@01;9 +35;3;9;030103;3;15000000-8 PRODUTOS ALIMENTARES, BEBIDAS, TABACO E PRODUTOS AFINS;15000000-8 PRODUTOS ALIMENTARES, BEBIDAS, TABACO E PRODUTOS AFINS;1;1;ANY;03;3;01;3@01;9 +36;3;9;030104;4;16000000-5 MAQUINARIA AGRÍCOLA;16000000-5 MAQUINARIA AGRÍCOLA;1;1;ANY;03;3;01;3@01;9 +37;3;9;030105;5;18000000-9 VESTUÁRIO, CALÇADO, MALAS E ARTIGOS DE VIAGEM, ACESSÓRIOS;18000000-9 VESTUÁRIO, CALÇADO, MALAS E ARTIGOS DE VIAGEM, ACESSÓRIOS;1;1;ANY;03;3;01;3@01;9 +38;3;9;030106;6;19000000-6 MATERIAIS TÊXTEIS, DE COURO, DE PLÁSTICO E DE BORRACHA;19000000-6 MATERIAIS TÊXTEIS, DE COURO, DE PLÁSTICO E DE BORRACHA;1;1;ANY;03;3;01;3@01;9 +39;3;9;030107;7;22000000-0 MATERIAL IMPRESSO E AFINS;22000000-0 MATERIAL IMPRESSO E AFINS;1;1;ANY;03;3;01;3@01;9 +40;3;9;030108;8;30100000-0 MÁQUINAS, EQUIPAMENTO E MATERIAL DE ESCRITÓRIO, EXCEPTO COMPUTADORES, IMPRESSORAS E MOBILIÁRIO;30100000-0 MÁQUINAS, EQUIPAMENTO E MATERIAL DE ESCRITÓRIO, EXCEPTO COMPUTADORES, IMPRESSORAS E MOBILIÁRIO;1;1;ANY;03;3;01;3@01;9 +41;3;9;030109;9;30200000-1 EQUIPAMENTO E MATERIAL INFORMÁTICO;30200000-1 EQUIPAMENTO E MATERIAL INFORMÁTICO;1;1;ANY;03;3;01;3@01;9 +42;3;9;030110;0;31000000-6 MAQUINARIA, APARELHAGEM, EQUIPAMENTO E CONSUMÍVEIS ELÉCTRICOS ILUMINAÇÃO;31000000-6 MAQUINARIA, APARELHAGEM, EQUIPAMENTO E CONSUMÍVEIS ELÉCTRICOS ILUMINAÇÃO;1;1;ANY;03;3;01;3@01;9 +43;3;9;030111;1;32000000-3 EQUIPAMENTO DE RÁDIO, TELEVISÃO, COMUNICAÇÃO, TELECOMUNICAÇÕES E AFINS;32000000-3 EQUIPAMENTO DE RÁDIO, TELEVISÃO, COMUNICAÇÃO, TELECOMUNICAÇÕES E AFINS;1;1;ANY;03;3;01;3@01;9 +44;3;9;030112;2;33000000-0 EQUIPAMENTO MÉDICO, MEDICAMENTOS E PRODUTOS PARA CUIDADOS PESSOAIS;33000000-0 EQUIPAMENTO MÉDICO, MEDICAMENTOS E PRODUTOS PARA CUIDADOS PESSOAIS;1;1;ANY;03;3;01;3@01;9 +45;3;9;030113;3;34000000-7 EQUIPAMENTO E PRODUTOS AUXILIARES DE TRANSPORTE;34000000-7 EQUIPAMENTO E PRODUTOS AUXILIARES DE TRANSPORTE;1;1;ANY;03;3;01;3@01;9 +46;3;9;030114;4;35000000-4 EQUIPAMENTO DE SEGURANÇA, COMBATE A INCÊNDIOS, POLÍCIA E DEFESA;35000000-4 EQUIPAMENTO DE SEGURANÇA, COMBATE A INCÊNDIOS, POLÍCIA E DEFESA;1;1;ANY;03;3;01;3@01;9 +47;3;9;030115;5;37000000-8 INSTRUMENTOS MUSICAIS, ARTIGOS DE DESPORTO, JOGOS, BRINQUEDOS, MATERIAL PARA ARTESANATO E ACTIVIDADES ARTÍSTICAS E ACESSÓRIOS;37000000-8 INSTRUMENTOS MUSICAIS, ARTIGOS DE DESPORTO, JOGOS, BRINQUEDOS, MATERIAL PARA ARTESANATO E ACTIVIDADES ARTÍSTICAS E ACESSÓRIOS;1;1;ANY;03;3;01;3@01;9 +48;3;9;030116;6;38000000-5 EQUIPAMENTO LABORATORIAL, ÓPTICO E DE PRECISÃO (EXC. ÓCULOS);38000000-5 EQUIPAMENTO LABORATORIAL, ÓPTICO E DE PRECISÃO (EXC. ÓCULOS);1;1;ANY;03;3;01;3@01;9 +49;3;9;030117;7;39000000-2 MOBILIÁRIO (INCL. DE ESCRITÓRIO), ACESSÓRIOS, APARELHOS DOMÉSTICOS (EXCL. ILUMINAÇÃO) E PRODUTOS DE LIMPEZA;39000000-2 MOBILIÁRIO (INCL. DE ESCRITÓRIO), ACESSÓRIOS, APARELHOS DOMÉSTICOS (EXCL. ILUMINAÇÃO) E PRODUTOS DE LIMPEZA;1;1;ANY;03;3;01;3@01;9 +50;3;9;030118;8;42000000-6 MÁQUINAS INDUSTRIAIS;42000000-6 MÁQUINAS INDUSTRIAIS;1;1;ANY;03;3;01;3@01;9 +51;3;9;030119;9;43000000-3 MAQUINARIA PARA EXTRACÇÃO MINEIRA E PEDREIRAS, EQUIPAMENTO DE CONSTRUÇÃO;43000000-3 MAQUINARIA PARA EXTRACÇÃO MINEIRA E PEDREIRAS, EQUIPAMENTO DE CONSTRUÇÃO;1;1;ANY;03;3;01;3@01;9 +52;3;9;030120;0;44000000-0 ESTRUTURAS E MATERIAIS DE CONSTRUÇÃO PRODUTOS AUXILIARES DE CONSTRUÇÃO (EXCEPTO APARELHOS ELÉCTRICOS);44000000-0 ESTRUTURAS E MATERIAIS DE CONSTRUÇÃO PRODUTOS AUXILIARES DE CONSTRUÇÃO (EXCEPTO APARELHOS ELÉCTRICOS);1;1;ANY;03;3;01;3@01;9 +53;3;9;030121;1;48000000-8 PACOTES DE SOFTWARE E SISTEMAS DE INFORMAÇÃO;48000000-8 PACOTES DE SOFTWARE E SISTEMAS DE INFORMAÇÃO;1;1;ANY;03;3;01;3@01;9 +54;3;9;030122;2;50000000-5 SERVIÇOS DE REPARAÇÃO E MANUTENÇÃO;50000000-5 SERVIÇOS DE REPARAÇÃO E MANUTENÇÃO;1;1;ANY;03;3;01;3@01;9 +55;3;9;030123;3;51000000-9 SERVIÇOS DE INSTALAÇÃO (EXCEPTO SOFTWARE);51000000-9 SERVIÇOS DE INSTALAÇÃO (EXCEPTO SOFTWARE);1;1;ANY;03;3;01;3@01;9 +56;3;9;030124;4;55000000-0 SERVIÇOS DE HOTELARIA, RESTAURAÇÃO E COMÉRCIO A RETALHO;55000000-0 SERVIÇOS DE HOTELARIA, RESTAURAÇÃO E COMÉRCIO A RETALHO;1;1;ANY;03;3;01;3@01;9 +57;3;9;030125;5;64000000-6 SERVIÇOS POSTAIS E DE TELECOMUNICAÇÕES;64000000-6 SERVIÇOS POSTAIS E DE TELECOMUNICAÇÕES;1;1;ANY;03;3;01;3@01;9 +58;3;9;030126;6;66000000-0 SERVIÇOS DE FINANÇAS E SEGUROS;66000000-0 SERVIÇOS DE FINANÇAS E SEGUROS;1;1;ANY;03;3;01;3@01;9 +59;3;9;030127;7;70000000-1 SERVIÇOS IMOBILIÁRIOS;70000000-1 SERVIÇOS IMOBILIÁRIOS;1;1;ANY;03;3;01;3@01;9 +60;3;9;030128;8;71000000-8 SERVIÇOS DE ARQUITECTURA, CONSTRUÇÃO, ENGENHARIA E INSPECÇÃO;71000000-8 SERVIÇOS DE ARQUITECTURA, CONSTRUÇÃO, ENGENHARIA E INSPECÇÃO;1;1;ANY;03;3;01;3@01;9 +61;3;9;030129;9;72000000-5 SERVIÇOS DE TI: CONSULTORIA, DESENVOLVIMENTO DE SOFTWARE, INTERNET E APOIO;72000000-5 SERVIÇOS DE TI: CONSULTORIA, DESENVOLVIMENTO DE SOFTWARE, INTERNET E APOIO;1;1;ANY;03;3;01;3@01;9 +62;3;9;030130;0;73000000-2 SERVIÇOS DE INVESTIGAÇÃO E DESENVOLVIMENTO E SERVIÇOS DE CONSULTORIA CONEXOS;73000000-2 SERVIÇOS DE INVESTIGAÇÃO E DESENVOLVIMENTO E SERVIÇOS DE CONSULTORIA CONEXOS;1;1;ANY;03;3;01;3@01;9 +63;3;9;030131;1;75000000-6 SERVIÇOS RELACIONADOS COM A ADMINISTRAÇÃO PÚBLICA, A DEFESA E A SEGURANÇA SOCIAL;75000000-6 SERVIÇOS RELACIONADOS COM A ADMINISTRAÇÃO PÚBLICA, A DEFESA E A SEGURANÇA SOCIAL;1;1;ANY;03;3;01;3@01;9 +64;3;9;030132;2;77000000-0 SERVIÇOS DE AGRICULTURA, SILVICULTURA, HORTICULTURA, AQUICULTURA E APICULTURA;77000000-0 SERVIÇOS DE AGRICULTURA, SILVICULTURA, HORTICULTURA, AQUICULTURA E APICULTURA;1;1;ANY;03;3;01;3@01;9 +65;3;9;030133;3;79000000-4 SERVIÇOS A EMPRESAS: DIREITO, COMERCIALIZAÇÃO, CONSULTORIA, RECRUTAMENTO, IMPRESSÃO E SEGURANÇA;79000000-4 SERVIÇOS A EMPRESAS: DIREITO, COMERCIALIZAÇÃO, CONSULTORIA, RECRUTAMENTO, IMPRESSÃO E SEGURANÇA;1;1;ANY;03;3;01;3@01;9 +66;3;9;030134;4;80000000-4 SERVIÇOS DE ENSINO E FORMAÇÃO;80000000-4 SERVIÇOS DE ENSINO E FORMAÇÃO;1;1;ANY;03;3;01;3@01;9 +67;3;9;030135;5;85000000-9 SERVIÇOS DE SAÚDE E ACÇÃO SOCIAL;85000000-9 SERVIÇOS DE SAÚDE E ACÇÃO SOCIAL;1;1;ANY;03;3;01;3@01;9 +68;3;9;030136;6;90000000-7 SERVIÇOS RELATIVOS A ÁGUAS RESIDUAIS, RESÍDUOS, LIMPEZA E AMBIENTE;90000000-7 SERVIÇOS RELATIVOS A ÁGUAS RESIDUAIS, RESÍDUOS, LIMPEZA E AMBIENTE;1;1;ANY;03;3;01;3@01;9 +69;3;9;030137;7;92000000-1 SERVIÇOS RECREATIVOS, CULTURAIS E DESPORTIVOS;92000000-1 SERVIÇOS RECREATIVOS, CULTURAIS E DESPORTIVOS;1;1;ANY;03;3;01;3@01;9 +70;3;9;030138;8;98000000-3 OUTROS SERVIÇOS COMUNITÁRIOS, SOCIAIS E PESSOAIS;98000000-3 OUTROS SERVIÇOS COMUNITÁRIOS, SOCIAIS E PESSOAIS;1;1;ANY;03;3;01;3@01;9 +71;3;10;030201;1;"DIVISÃO V - ELECTRICIDADE, GÁS E ÁGUA; GRUPO 3 — CAPTAÇÃO E DISTRIBUIÇÃO DE ÁGUAS; CÓDIGOS 1295 A 1335";"DIVISÃO V - ELECTRICIDADE, GÁS E ÁGUA; GRUPO 3 — CAPTAÇÃO E DISTRIBUIÇÃO DE ÁGUAS; CÓDIGOS 1295 A 1335";1;1;ANY;03;3;02;3@02;10 +72;3;10;030202;2;"GRUPO 1 — IMÓVEIS; CÓDIGOS 2005 A 2090";"GRUPO 1 — IMÓVEIS; CÓDIGOS 2005 A 2090";1;1;ANY;03;3;02;3@02;10 +73;3;10;030203;3;"GRUPO 2 — INSTALAÇÕES; CÓDIGOS 2095 A 2195";"GRUPO 2 — INSTALAÇÕES; CÓDIGOS 2095 A 2195";1;1;ANY;03;3;02;3@02;10 +74;3;10;030204;4;"GRUPO 3 — MÁQUINAS, APARELHOS E FERRAMENTAS; CÓDIGOS 2200 A 2315";"GRUPO 3 — MÁQUINAS, APARELHOS E FERRAMENTAS; CÓDIGOS 2200 A 2315";1;1;ANY;03;3;02;3@02;10 +75;3;10;030205;5;"GRUPO 4 — MATERIAL ROLANTE OU DE TRANSPORTE; CÓDIGOS 2375 A 2390";"GRUPO 4 — MATERIAL ROLANTE OU DE TRANSPORTE; CÓDIGOS 2375 A 2390";1;1;ANY;03;3;02;3@02;10 +76;3;10;030206;6;"GRUPO 5 — ELEMENTOS DIVERSOS; CÓDIGOS 2400 A 2455";"GRUPO 5 — ELEMENTOS DIVERSOS; CÓDIGOS 2400 A 2455";1;1;ANY;03;3;02;3@02;10 +77;3;13;030501;1;QUANTIDADE DE RESÍDUOS URBANOS DE RECOLHA INDIFERENCIADA PRODUZIDA;QUANTIDADE DE RESÍDUOS URBANOS DE RECOLHA INDIFERENCIADA PRODUZIDA;1;1;ANY;03;3;05;3@05;13 +78;3;13;030502;2;VOLUME DE ÁGUA CONSUMIDA;VOLUME DE ÁGUA CONSUMIDA;1;1;ANY;03;3;05;3@05;13 +79;3;14;030601;1;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS A GASÓLEO;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS A GASÓLEO;1;1;ANY;03;3;06;3@06;14 +80;3;14;030602;2;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS A GASOLINA;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS A GASOLINA;1;1;ANY;03;3;06;3@06;14 +81;3;14;030603;3;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS HÍBRIDOS;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS HÍBRIDOS;1;1;ANY;03;3;06;3@06;14 +82;3;14;030604;4;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS ELÉTRICOS;RENT-A-CAR: DISTÂNCIA PERCORRIDA - VEÍCULOS ELÉTRICOS;1;1;ANY;03;3;06;3@06;14 +83;3;14;030605;5;VIA AÉREA - DISTÂNCIA PERCORRIDA - VIAGENS DE CURTA DISTÂNCIA (< 1600 KM);VIA AÉREA - DISTÂNCIA PERCORRIDA - VIAGENS DE CURTA DISTÂNCIA (< 1600 KM);1;1;ANY;03;3;06;3@06;14 +84;3;14;030606;6;VIA AÉREA - DISTÂNCIA PERCORRIDA - VIAGENS DE MÉDIA DISTÂNCIA (1600 – 4000 KM);VIA AÉREA - DISTÂNCIA PERCORRIDA - VIAGENS DE MÉDIA DISTÂNCIA (1600 – 4000 KM);1;1;ANY;03;3;06;3@06;14 +85;3;14;030607;7;VIA AÉREA - DISTÂNCIA PERCORRIDA – VIAGENS DE LONGA DISTÂNCIA (> 4000 KM);VIA AÉREA - DISTÂNCIA PERCORRIDA – VIAGENS DE LONGA DISTÂNCIA (> 4000 KM);1;1;ANY;03;3;06;3@06;14 +86;3;14;030608;8;VIA AÉREA - EMISSÕES TOTAIS (SE FORNECIDOS POR AGÊNCIA DE VIAGENS);VIA AÉREA - EMISSÕES TOTAIS (SE FORNECIDOS POR AGÊNCIA DE VIAGENS);1;1;ANY;03;3;06;3@06;14 +87;3;14;030609;9;TRANSPORTE FERROVIÁRIO – DISTÂNCIA PERCORRIDA - COMBOIOS NACIONAIS;TRANSPORTE FERROVIÁRIO – DISTÂNCIA PERCORRIDA - COMBOIOS NACIONAIS;1;1;ANY;03;3;06;3@06;14 +88;3;14;030610;0;TRANSPORTE FERROVIÁRIO – DISTÂNCIA PERCORRIDA - COMBOIOS INTERNACIONAIS;TRANSPORTE FERROVIÁRIO – DISTÂNCIA PERCORRIDA - COMBOIOS INTERNACIONAIS;1;1;ANY;03;3;06;3@06;14 +89;3;15;030701;1;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASÓLEO;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASÓLEO;1;1;ANY;03;3;07;3@07;15 +90;3;15;030702;2;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASOLINA;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASOLINA;1;1;ANY;03;3;07;3@07;15 +91;3;15;030703;3;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS – GÁS NATURAL / GPL AUTO;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS – GÁS NATURAL / GPL AUTO;1;1;ANY;03;3;07;3@07;15 +92;3;15;030704;4;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - ELÉTRICO;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - ELÉTRICO;1;1;ANY;03;3;07;3@07;15 +93;3;15;030705;5;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - HÍBRIDO;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - HÍBRIDO;1;1;ANY;03;3;07;3@07;15 +94;3;15;030707;7;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO A GASOLINA;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO A GASOLINA;1;1;ANY;03;3;07;3@07;15 +95;3;15;030708;8;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO ELÉTRICO;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO ELÉTRICO;1;1;ANY;03;3;07;3@07;15 +96;3;15;030709;9;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO;1;1;ANY;03;3;07;3@07;15 +97;3;15;030711;1;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO ELÉTRICO / METROBUS;DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO ELÉTRICO / METROBUS;1;1;ANY;03;3;07;3@07;15 +98;3;15;030712;2;DISTÂNCIA PERCORRIDA | TRANSPORTE FERROVIÁRIO – COMBÓIO / METRO;DISTÂNCIA PERCORRIDA | TRANSPORTE FERROVIÁRIO – COMBÓIO / METRO;1;1;ANY;03;3;07;3@07;15 +99;3;15;030713;3;DISTÂNCIA PERCORRIDA | BICICLETA/ MODOS SUAVES;DISTÂNCIA PERCORRIDA | BICICLETA/ MODOS SUAVES;1;1;ANY;03;3;07;3@07;15 +100;3;15;030714;4;DISTÂNCIA PERCORRIDA | BARCO;DISTÂNCIA PERCORRIDA | BARCO;1;1;ANY;03;3;07;3@07;15 +101;3;15;030715;5;DISTÂNCIA PERCORRIDA | A PÉ;DISTÂNCIA PERCORRIDA | A PÉ;1;1;ANY;03;3;07;3@07;15 +102;3;17;030901;1;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASÓLEO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASÓLEO;1;1;ANY;03;3;09;3@09;17 +103;3;17;030902;2;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASOLINA;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - GASOLINA;1;1;ANY;03;3;09;3@09;17 +104;3;17;030903;3;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS – GÁS NATURAL / GPL AUTO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS – GÁS NATURAL / GPL AUTO;1;1;ANY;03;3;09;3@09;17 +105;3;17;030904;4;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - ELÉTRICO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - ELÉTRICO;1;1;ANY;03;3;09;3@09;17 +106;3;17;030905;5;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - HÍBRIDO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - LIGEIRO PASSAGEIROS - HÍBRIDO;1;1;ANY;03;3;09;3@09;17 +107;3;17;030906;6;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO A GASOLINA;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO A GASOLINA;1;1;ANY;03;3;09;3@09;17 +108;3;17;030907;7;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO ELÉTRICO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO - MOTOCICLO ELÉTRICO;1;1;ANY;03;3;09;3@09;17 +109;3;17;030908;8;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO;1;1;ANY;03;3;09;3@09;17 +110;3;17;030909;9;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO ELÉTRICO / METROBUS;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE RODOVIÁRIO COLETIVO – AUTOCARRO ELÉTRICO / METROBUS;1;1;ANY;03;3;09;3@09;17 +111;3;17;030910;0;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE FERROVIÁRIO – COMBÓIO / METRO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | TRANSPORTE FERROVIÁRIO – COMBÓIO / METRO;1;1;ANY;03;3;09;3@09;17 +112;3;17;030911;1;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | BICICLETA/ MODOS SUAVES;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | BICICLETA/ MODOS SUAVES;1;1;ANY;03;3;09;3@09;17 +113;3;17;030912;2;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | BARCO;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | BARCO;1;1;ANY;03;3;09;3@09;17 +114;3;17;030913;3;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | A PÉ;TRANSPORTE DE ALUNOS | DISTÂNCIA PERCORRIDA | A PÉ;1;1;ANY;03;3;09;3@09;17 +115;4;24;990101;1;ELETRICIDADE RENOVÁVEL PRODUZIDA E VENDIDA À REDE PÚBLICA;ELETRICIDADE RENOVÁVEL PRODUZIDA E VENDIDA À REDE PÚBLICA;1;1;ANY;99;4;01;4@01;24 +116;4;24;990102;2;ELETRICIDADE RENOVÁVEL PRODUZIDA PARA AUTOCONSUMO;ELETRICIDADE RENOVÁVEL PRODUZIDA PARA AUTOCONSUMO;1;1;ANY;99;4;01;4@01;24 +117;4;24;990103;3;ELETRICIDADE RENOVÁVEL PRODUZIDA E ENCAMINHADA VIA CER (COMUNIDADE DE ENERGIA RENOVÁVEL);ELETRICIDADE RENOVÁVEL PRODUZIDA E ENCAMINHADA VIA CER (COMUNIDADE DE ENERGIA RENOVÁVEL);1;1;ANY;99;4;01;4@01;24 +118;4;24;990104;4;ELETRICIDADE RENOVÁVEL CONSUMIDA VIA CER (COMUNIDADE DE ENERGIA RENOVÁVEL);ELETRICIDADE RENOVÁVEL CONSUMIDA VIA CER (COMUNIDADE DE ENERGIA RENOVÁVEL);1;1;ANY;99;4;01;4@01;24 diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml new file mode 100644 index 0000000..4909c76 --- /dev/null +++ b/src/main/resources/config/liquibase/master.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/tls/keystore.p12 b/src/main/resources/config/tls/keystore.p12 new file mode 100644 index 0000000..1627128 Binary files /dev/null and b/src/main/resources/config/tls/keystore.p12 differ diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..eeea3e9 --- /dev/null +++ b/src/main/resources/i18n/messages.properties @@ -0,0 +1,48 @@ +# Error page +error.title=Your request cannot be processed +error.subtitle=Sorry, an error has occurred. +error.status=Status: +error.message=Message: + +# Activation email +email.activation.title=resilient account activation is required +email.activation.greeting=Dear {0} +email.activation.text1=Your resilient account has been created, please click on the URL below to activate it: +email.activation.text2=Regards, +email.signature=resilient Team. + +# Creation email +email.creation.text1=Your resilient account has been created, please click on the URL below to access it: + +# Reset email +email.reset.title=resilient password reset +email.reset.greeting=Dear {0} +email.reset.text1=For your resilient account a password reset was requested, please click on the URL below to reset it: +email.reset.text2=Regards, + +# Error +E001=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The class code [{3}] wasn't found. +E002=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The unit code [{3}] it's not allowed in the variable. +E003=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The value [{3}] it's invalid for type [{4}]. +E004=Sheet [{0}]; Row[{1}]; The variable code [{2}] wasn't found. +E005=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The unit code [{3}] wasn't found. +E006=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The value needs convertion to the base unit, but no converter was found between units: [{3}] and [{4}]. +E007=Emission Factor not found for: Code={0}; Year={1} +E008=An error occurred evaluating the expression: [{0}]. With the following error: {1} +E009=An error occurred calculating the output: Variable Code={0}, Organization Code={1}, Period={2}, Period Version={3}, Expression={4}. With the error: {5} +E010=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; Unit convertion formula exception to convert the value from the unit [{3}] in the unit [{4}], with formula [{5}]. +E011=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; Unable to read the value [{4}] has type [{3}]. +E012=Dependncias de variveis em formulas no satisfeitas. [{0}] +E013=An error occurred calculating the unit converter: Variable Code [{0}], From Unit Code [{1}], To Unit Code [{2}], Value [{3}]. With the following error: {4} +E014=Sheet [{0}]; Row[{1}]; Organization Code [{2}]. Organization not found. +E015=Sheet [{0}]; Row [{1}]; Variable [{2}]; Input Org. Code [{3}]; Importer Org. Code [{4}]. Input Organization must be a children of the Importer Organization. +E016=The expression evaluation resulted in null value: Variable Code [{0}], Organization Code [{1}], Period [{2}], Period Version [{3}], Expression [{4}]. +E017=Resilient JPA method not implemented. Entity JPA interfaces must provide custom eager loading methods. See documentation for {0}.{1}. +E018=Variable Code [{0}]; A unit convertion is needed, but no converter is configured to convert the value from the unit [{1}] in the unit [{2}]. +E019=Variable Code [{0}]; The unit converter from unit [{1}] to unit [{2}] exists, but doesn't have a convertion formula defined. This is a configuration error. +E020=Variable Code [{0}]; The unit converter from unit [{1}] to unit [{2}] found multiple converters. Only one is expected. This is a configuration error. +E021=Variable Code [{0}]; This variable is not configured from input. +E022=Variable Code [{0}]; This variable is not configured to allow insertion from a datafile. +E023=Period [{0}] not ready to receive data. Expected state [{1}] but it's current state is [{2}]. +E024=The InputDataUpload configured organization is NOT the same UO within the file. The configured organization code is [{0}] and the file contains [{1}]. +E025=The InputDataUpload configured period year is NOT the same year within the file. The period year is [{0}] and the file year is [{1}]. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties new file mode 100644 index 0000000..1ca022e --- /dev/null +++ b/src/main/resources/i18n/messages_en.properties @@ -0,0 +1,48 @@ +# Error page +error.title=Your request cannot be processed +error.subtitle=Sorry, an error has occurred. +error.status=Status: +error.message=Message: + +# Activation email +email.activation.title=resilient account activation +email.activation.greeting=Dear {0} +email.activation.text1=Your resilient account has been created, please click on the URL below to activate it: +email.activation.text2=Regards, +email.signature=resilient Team. + +# Creation email +email.creation.text1=Your resilient account has been created, please click on the URL below to access it: + +# Reset email +email.reset.title=resilient password reset +email.reset.greeting=Dear {0} +email.reset.text1=For your resilient account a password reset was requested, please click on the URL below to reset it: +email.reset.text2=Regards, + +# Error +E001=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The class code [{3}] wasn't found. +E002=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The unit code [{3}] it's not allowed in the variable. +E003=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The value [{3}] it's invalid for type [{4}]. +E004=Sheet [{0}]; Row[{1}]; The variable code [{2}] wasn't found. +E005=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The unit code [{3}] wasn't found. +E006=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; The value needs convertion to the base unit, but no converter was found between units: [{3}] and [{4}]. +E007=Emission Factor not found for: Code={0}; Year={1} +E008=An error occurred evaluating the expression: [{0}]. With the following error: {1} +E009=An error occurred calculating the output: Variable Code={0}, Organization Code={1}, Period={2}, Period Version={3}, Expression={4}. With the error: {5} +E010=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; Unit convertion formula exception to convert the value from the unit [{3}] in the unit [{4}], with formula [{5}]. +E011=Sheet [{0}]; Row[{1}]; Variable Code [{2}]; Unable to read the value [{4}] has type [{3}]. +E012=Dependencies not satisfyed. [{0}] +E013=An error occurred calculating the unit converter: Variable Code [{0}], From Unit Code [{1}], To Unit Code [{2}], Value [{3}]. With the following error: {4} +E014=Sheet [{0}]; Row[{1}]; Organization Code [{2}]. Organization not found. +E015=Sheet [{0}]; Row [{1}]; Variable [{2}]; Input Org. Code [{3}]; Importer Org. Code [{4}]. Input Organization must be a children of the Importer Organization. +E016=The expression evaluation resulted in null value: Variable Code [{0}], Organization Code [{1}], Period [{2}], Period Version [{3}], Expression [{4}]. +E017=Resilient JPA method not implemented. Entity JPA interfaces must provide custom eager loading methods. See documentation for {0}.{1}. +E018=Variable Code [{0}]; A unit convertion is needed, but no converter is configured to convert the value from the unit [{1}] in the unit [{2}]. +E019=Variable Code [{0}]; The unit converter from unit [{1}] to unit [{2}] exists, but doesn't have a convertion formula defined. This is a configuration error. +E020=Variable Code [{0}]; The unit converter from unit [{1}] to unit [{2}] found multiple converters. Only one is expected. This is a configuration error. +E021=Variable Code [{0}]; This variable is not configured from input. +E022=Variable Code [{0}]; This variable is not configured to allow insertion from a datafile. +E023=Period [{0}] not ready to receive data. Expected state [{1}] but it's current state is [{2}]. +E024=The InputDataUpload configured organization is NOT the same UO within the file. The configured organization code is [{0}] and the file contains [{1}]. +E025=The InputDataUpload configured period year is NOT the same year within the file. The period year is [{0}] and the file year is [{1}]. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_pt_PT.properties b/src/main/resources/i18n/messages_pt_PT.properties new file mode 100644 index 0000000..6e5bde1 --- /dev/null +++ b/src/main/resources/i18n/messages_pt_PT.properties @@ -0,0 +1,48 @@ +# Error page +error.title=O seu pedido não pode ser processado +error.subtitle=Desculpe, ocorreu um erro. +error.status=Estado: +error.message=Mensagem: + +# Activation email +email.activation.title=resilient ativação +email.activation.greeting=Caro {0} +email.activation.text1=A sua conta resilient foi criada, por favor clique na ligação abaixo para ativar: +email.activation.text2=Atenciosamente, +email.signature=Equipe resilient. + +# Creation email +email.creation.text1=A sua conta resilient foi criada, clique na ligação abaixo para acessá-la: + +# Reset email +email.reset.title=A palavra-passe da aplicação resilient foi redefinida +email.reset.greeting=Caro {0} +email.reset.text1=Foi solicitada a recuperação da palavra-passe da sua conta resilient. Por favor clique na ligação abaixo para alterá-la: +email.reset.text2=Atenciosamente, + +# Error +E001=Folha [{0}]; Linha[{1}]; Variável [{2}]; O código de classe [{3}] não foi encontrado. +E002=Folha [{0}]; Linha[{1}]; Variável [{2}]; O código de unidade [{3}] não é permitido para esta variável. +E003=Folha [{0}]; Linha[{1}]; Variável [{2}]; O valor [{3}] não é válido para o tipo [{4}]. +E004=Folha [{0}]; Linha[{1}]; Variável [{2}]; O código de variável [{3}] não foi encontrado. +E005=Folha [{0}]; Linha[{1}]; Variável [{2}]; O código de unidade [{3}] não foi encontrado. +E006=Folha [{0}]; Linha[{1}]; Variável [{2}]; É necessário converter o valor na unidade base da variável, mas não foi encontrado conversor entre a unidade [{3}] e a unidade [{4}]. +E007=Factor de Emissão não encontrado para: Código={0}; Ano={1} +E008=Ocorreu um erro na avaliação da expressão: [{0}]. Com o erro: {1} +E009=Ocorreu um erro no calculo do output: Cód. Variável={0}, Cod. Organização={1}, Periodo={2}, Versão={3}, Expressão={4}. Com o erro: {5} +E010=Folha [{0}]; Linha[{1}]; Variável [{2}]; Ocorreu um erro na avaliação da formula do conversor entre a unidade [{3}] e a unidade [{4}], com a fórmula [{5}]. +E011=Folha [{0}]; Linha[{1}]; Variável [{2}]; Ocorreu um erro na leitura do valor [{4}] para o tipo [{3}]. +E012=Dependências de variáveis em formulas não satisfeitas. [{0}] +E013=Ocorreu um erro no calculo da conversão de unidade: Variável [{0}], Da Unidade [{1}], Para Unidade [{2}], Valor [{3}]. Com o erro: {4} +E014=Folha [{0}]; Linha[{1}]; Organização [{2}]. Organização não encontrada. +E015=Folha [{0}]; Linha[{1}]; Variável [{2}]; Organização Input [{3}]; Organização Importer [{4}]. Organização Input tem de ser descendente da Organização Importer. +E016=A expressão avaliada teve como resultado o valor NULL: Variável [{0}], Organização [{1}], Periodo [{2}], Versão Periodo [{3}], Fórmula [{4}]. +E017=Resilient JPA method not implemented. Entity JPA interfaces must provide custom eager loading methods. See documentation for {0}.{1}. +E018=Variável [{0}]; É necessário converter o valor na unidade base da variável, mas não foi encontrado conversor entre a unidade [{1}] e a unidade [{2}]. +E019=Variável [{0}]; O conversor de valor entre a unidade [{1}] e a unidade [{2}] foi encontrado, mas não tem fórmula de conversão configurada. Isto é um erro de configuração.. +E020=Variável [{0}]; Foram encontrados multiplos conversores de valor entre a unidade [{1}] e a unidade [{2}]. Apenas é esperado um. Isto é um erro de configuração. +E021=Variável [{0}]; Não está configurada como input. +E022=Variável [{0}]; Não está configurada para permitir insersão por upload de ficheiro. +E023=O Periodo [{0}] não está pronto para receber dados. Estado esperado [{1}] mas atualmente tem o estado [{2}]. +E024=A Organização configurada no registo de upload não coincide a UO do ficheiro. O código configurado é [{0}] e o ficheiro contém [{1}]. +E025=O Ano configurado no periodo associado ao registo de upload não coincide com o ano do dados do ficheiro. O ano do periodo é [{0}] e o ficheiro contém o ano [{1}]. \ No newline at end of file diff --git a/src/main/resources/keystore/keystore.p12 b/src/main/resources/keystore/keystore.p12 new file mode 100644 index 0000000..361e0da Binary files /dev/null and b/src/main/resources/keystore/keystore.p12 differ diff --git a/src/main/resources/keystore/readme b/src/main/resources/keystore/readme new file mode 100644 index 0000000..c4440a3 --- /dev/null +++ b/src/main/resources/keystore/readme @@ -0,0 +1,9 @@ +Command to generate a new keystore with a selfsign certificate: + +> keytool -genkeypair -alias innova-ssl-cert -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore keystore.p12 -validity 3650 -storepass nova#123 -keypass nova#123 -dname "CN=localhost, OU=Dev, O=NOVA, L=Lisbon, S=Lisbon, C=PT" + +Para exportar o *.crt e *.key, para usar no servidor httpd ou revers-proxy: + +# Extract key and cert from keystore.p12 +> openssl pkcs12 -in keystore.p12 -nokeys -out nova_dev.crt +> openssl pkcs12 -in keystore.p12 -nocerts -nodes -out nova_dev.key diff --git a/src/main/resources/keystore/resilient_dev.crt b/src/main/resources/keystore/resilient_dev.crt new file mode 100644 index 0000000..559e295 --- /dev/null +++ b/src/main/resources/keystore/resilient_dev.crt @@ -0,0 +1,26 @@ +Bag Attributes + friendlyName: innova-ssl-cert + localKeyID: 54 69 6D 65 20 31 37 34 36 34 35 33 36 36 31 33 39 33 +subject=C = PT, ST = Lisbon, L = Lisbon, O = NOVA, OU = Dev, CN = localhost +issuer=C = PT, ST = Lisbon, L = Lisbon, O = NOVA, OU = Dev, CN = localhost +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIIDrjgAqiJU9cwDQYJKoZIhvcNAQELBQAwYDELMAkGA1UE +BhMCUFQxDzANBgNVBAgTBkxpc2JvbjEPMA0GA1UEBxMGTGlzYm9uMQ0wCwYDVQQK +EwROT1ZBMQwwCgYDVQQLEwNEZXYxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0yNTA1 +MDUxNDAxMDFaFw0zNTA1MDMxNDAxMDFaMGAxCzAJBgNVBAYTAlBUMQ8wDQYDVQQI +EwZMaXNib24xDzANBgNVBAcTBkxpc2JvbjENMAsGA1UEChMETk9WQTEMMAoGA1UE +CxMDRGV2MRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCuBBTd/KrlyBw+DHlcelSn8UpQp7IJttyME6JhhNx9i5bRzMOT +vlTVyyK3DlEjUxiXxcd31K1GWPSO57/kpnOP1ors02dofwIfwIy2Mp+bDB9gaydY +zVtNy0gqFKP0zaW4zxQFI77RsrGO7Uhly4zcqY+G3haQq/7i3pWonFusi8llYLAk +1QPTGMiCwWIssS88ouvV9S2SCbsuMdHO+sERdY8179wdlvlQ4mgvzwBls1U/upwE +MGUh+Mt0ZltOKJSP04uscq9JTIlCWekHfzI1yoF1LSINYifOdW5qNMGICbvSbCPr +DpIR8PSZ+SR9BedELcJOHv2LtA9eAkZCxrc5AgMBAAGjITAfMB0GA1UdDgQWBBRf +OowF84st9IoDkH9LQZhSI65EwTANBgkqhkiG9w0BAQsFAAOCAQEAaaQ5JDDld+eO +f0y0AodqfsY9ycZ10RGLYRHN/hXOrQnLQNHaWQOz3ElSrF7OArCC2lw+agVW5wiy +L5WAmh6Kdb5wcgmxYbNjJ4eEEWR4MzFMq8O3vgdcM2940I5r3PMF3HKUFq6NBivh +iJlV8vub+a7hdR7/niXnlbHq1Yaf0MpKQAQoRiLKYD1LMXo3h41yo9ocXGNoNN2d +VWFEXoaFXMN3C6A0keEASHoCbZzNDpoRk8yZMBLmdS0nxCY5xUWIjwg4FCGV0Wcu +lmjzQRiBjy132V4kjV2HvFJqPgyLgc9fac7qp7ZqWwkuUXN/slhPADCuHwR+n5NN +4pM/AI4sMg== +-----END CERTIFICATE----- diff --git a/src/main/resources/keystore/resilient_dev.key b/src/main/resources/keystore/resilient_dev.key new file mode 100644 index 0000000..36345b8 --- /dev/null +++ b/src/main/resources/keystore/resilient_dev.key @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: innova-ssl-cert + localKeyID: 54 69 6D 65 20 31 37 34 36 34 35 33 36 36 31 33 39 33 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCuBBTd/KrlyBw+ +DHlcelSn8UpQp7IJttyME6JhhNx9i5bRzMOTvlTVyyK3DlEjUxiXxcd31K1GWPSO +57/kpnOP1ors02dofwIfwIy2Mp+bDB9gaydYzVtNy0gqFKP0zaW4zxQFI77RsrGO +7Uhly4zcqY+G3haQq/7i3pWonFusi8llYLAk1QPTGMiCwWIssS88ouvV9S2SCbsu +MdHO+sERdY8179wdlvlQ4mgvzwBls1U/upwEMGUh+Mt0ZltOKJSP04uscq9JTIlC +WekHfzI1yoF1LSINYifOdW5qNMGICbvSbCPrDpIR8PSZ+SR9BedELcJOHv2LtA9e +AkZCxrc5AgMBAAECggEAAInRMbWeJrtVgQijWHqlZ+aacwDtz1NiP8mwwyn4z6ee +phQC1+JrG0U3XIceD6Sqaw/I0/FTvue4C/giExhDu8JvaBeoVn2sGUKMfOTPsxhY +wYDbXI44FdfG56BsOG3pcRas6m4noTjzDSzlQDFexHD+2W029ygdEAEdx+mB7Bj6 +Tm5LbBlwKoIqOmrJ9kHUyi63RQj135m3yrHCGzsecEU+HJDPGaYRPz89068Ja5la +3uh6yhm5SFOn/4Oj4vx1/dABtCvIsDXoaVJZVprVqPVX1AmnBSedlbKFkNkqrz2+ +D03aTdFa1XGghqVbguPpO6qXTh6JKhlt9QONM2q44QKBgQDVj7p1megCiojC49cf +pYesbT2/pYFLX6jPQZCU6P0Ty0zLX6UEE5FEe9JkoMfA+6ytf82zweJlF2U/JWXs +J9+AQU22rACMSLTYNzRxzjlcEUbm133HHjoK77zN6T1rcPLbf51hkKRyIqxH2DHR +qoySz1I8ueRkd8PfuWocgyrsEQKBgQDQmJl43IrQvUEft4PSue3Kw2SaCabSBXCG +ZIRy49ACArHpAuaSBCD8FwzDkv0D5gxgFBRuUC1Ru70e4AfRyp73jVdo0mABjJ++ +pIs4yAxeIz9gDcS/PeZ4BCZ1vPap2zTMsZ2yCglXbeWzCocLzTStDx8b4hT96xl3 +XoHLuMLgqQKBgGm5/yiLakYfyXcajvzW2SUu69v+FtV25T6+CdL+yZPMGReyISK0 +gT1zQ4SX6GyM7D70v6SXfvpnK7OoUx4n20lGiy+9VYQd1pIYOnmBC/qdfwl3c4hp +WXjimQkjyElxoHm0YvjTP+ZVbg8fZAKVuYRQ6TixGvX2KN2QkIzPDyfRAoGAbr+G +RBf7j4XbjKZXU8spSAzjXbEgI8OFkaTOeV0gc+DQamEC36VXieAHA9MHiElcaqpe +io8A8LMXXswc+rJ0IgSl0t5W4JuzuHkN/bCgeF6IaEwHGG4Z7cBuVvZjk5zxdHgc +vIj9mrCbUqqVNpvVishLgPdQo9ttYuYHTY0j0jkCgYEAvqddASppkAb4hYHwA03C +PAEhErq13hb1+RdIOmCKwIvDrnti9MKTpCDQ1LXa2gIWy4DxiOWUo3Ryvwr/Dov7 +1de1+1jNmbIq5bzaGzlbYoWeBczpil5DXhVH5qVrWgcmMykUCGPEO6PCyj/V8EMg +z9Yfk3XR9DNMfe6ry2IOqM4= +-----END PRIVATE KEY----- diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..45826df --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/main/resources/mock-idp/certs/idp-private.key b/src/main/resources/mock-idp/certs/idp-private.key new file mode 100644 index 0000000..686c001 --- /dev/null +++ b/src/main/resources/mock-idp/certs/idp-private.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDyxkb2UrPwNyh4 +LFdNhmvP/wNUNnymDfZbUsigT3Klsw1fM15V21g89C6pqFS2fP8R9bHDK5Q9xGdj +1EpdmHZzrbuY3x4MadIThuUgTuRrdNl/TzeRAy/UeAHwWJhtc+XQomhwZp5sLOcm +yEe8MifopyyVcVGn48wDXE+da2VxBsidE4fGyNYO+VnL/37n1FdVPR8FiLgWWYq2 +EbWT5Vg7N6JRaV5VIUPcpnHrITG64+Xo9wQYCx8PqmcEe0RHK/8J4SvsNkVqADSC +kCg4wsg5AK7xbcXzPcxdwmnikLWT1HJy6MiPCqrmKW9rcv4rlxNq7etU5VeEMLr4 +QSe/wgmnAgMBAAECggEAQWB8vpuh4jfwWYBTWEixIteBHX34zjznUUt3RJhwfse7 +e54ZMtS5K9zz7fMrMONzSvJXlv/W0VVhJESIbDEBAQDRiobXEC+1B1YlwLAOGhPi ++EIsbAwoJrbUitVI4vy5cBg0OMSht+7Vpp97leYJ0kCmpG3aN/SDvYnv1KwVqrxL +JtODOyCuBlttIH6uo1e6aU6HKWTwRDp177RSLuYOjBmQi6Hxwa+w31zQ6Q8Scubb +S0z61C3L8Azdrb8B4/LfoARIDhy/gVvq7W3gIRkUZCl9oFBehpV+TT1BFPVGiKdm +3vasOeo7BEORglbj+IaCovBooIGsUkDA1aZFCVx/3QKBgQD3e+owizU9Jp1j+8vl +FtzHmbjBrNFIy4wiiMN0L1cKQzzVig7vb1JIwbK+uDg9csulObsdtva1FU4wFJOk +R8JULJ0y4DRY1piT2p2bQA16IvSLvMrUlZ5KT1TqimsWxmkiodlBm3UM+q7rivca +hSftmmayuNcjHj9FWKBM/6J/vQKBgQD7IOBRv2OgHcibQ5LdGX8TQqw55XKAmiEq +nQI4Jk1XsTwdsdd+aqo81gp6Fdk3NdXM2x+kMDsP6IziaXyubI4w8Kkj6YJTL59p +V3PACdfqToEkTblKdYVE7yN6gsuOT4qi0aedSAsRmeQpb8GMHjalgc5pv4Y34W8+ +E4LY4nbjMwKBgQCcLZjW1aLdWlcM18QOaGUfmUTdBEB2ne1rhb9CvPVCxrfHUn6m +XywgOgyhCwSC0sTtGgeZcvMxx6Y19WZOz/I0yIrTpmWigpp7BAVeCgf3QcPtw1CE +436nCnVeJcf68W87qcO/AWnWrQRiJKpYFBvkeAHDW554zQfErW9L6C8WSQKBgAoF +Ms4wO9Jsvc9sL9UAqnBjTan1vM7i14Xyw97nsFhaaxKoQPf7W5WX2M0sSAGK9V/6 +MlYD0qd82PpDyUTQchAD2kvjil61XMAATE8SVXo07bQ8IbOV4t5wSFMgGu0vwVFj +2jNNZ5upL1Bz9B4aKoYKGulfSgS6ywyIDMWIq8O/AoGAC1Ax4BQyB4/Jp2+oWtKF +tkTdYPR3kmZtrtwhhYVDC+zXl/zSRlmRLNaHUpRyND/JRm/ngsYFrM1ZiRuiLTR+ +aYbKwy1AJh0HU2heH/5AL+mDbOkvHPkxiKRA+b58mUclMpll5x2l7sZ2hBg/Qspw +6NjlEMHxyqq9Viz4DNxbQ20= +-----END PRIVATE KEY----- diff --git a/src/main/resources/mock-idp/certs/idp-public.cert b/src/main/resources/mock-idp/certs/idp-public.cert new file mode 100644 index 0000000..4cec70a --- /dev/null +++ b/src/main/resources/mock-idp/certs/idp-public.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUJGNdh9cxePuEzWTUlEuSOkwfwyUwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDQyNDA5MjI1MFoXDTI2MDQy +NDA5MjI1MFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8sZG9lKz8DcoeCxXTYZrz/8DVDZ8pg32W1LIoE9ypbMN +XzNeVdtYPPQuqahUtnz/EfWxwyuUPcRnY9RKXZh2c627mN8eDGnSE4blIE7ka3TZ +f083kQMv1HgB8FiYbXPl0KJocGaebCznJshHvDIn6KcslXFRp+PMA1xPnWtlcQbI +nROHxsjWDvlZy/9+59RXVT0fBYi4FlmKthG1k+VYOzeiUWleVSFD3KZx6yExuuPl +6PcEGAsfD6pnBHtERyv/CeEr7DZFagA0gpAoOMLIOQCu8W3F8z3MXcJp4pC1k9Ry +cujIjwqq5ilva3L+K5cTau3rVOVXhDC6+EEnv8IJpwIDAQABo1MwUTAdBgNVHQ4E +FgQURDfHxj6ACRLLPcVRTgvlwhJSnyAwHwYDVR0jBBgwFoAURDfHxj6ACRLLPcVR +TgvlwhJSnyAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABioO +OCok/9JigKTupTEnJLxgueSIqk0lWXa7E6zwHLJh+DUUSlS1kECL2+s+HlGz7oCj +uiemCLMssTd0ZOn95GN5b4IbEl8r+12hVaREKDC7qlydkFnKKomvEVQJUbAKPgqv +EhNmJsp9TPKQzCdwTz00g0mmZCtc4cJoXEiOR+TKf/AwxXbh8/++9big1hIAnpHC +zQgfebBg+I+1FPuUsqC6+zp4DrAflpQ2q0/4SNGyaPumf2K3ZjxLd9MPi+acd/HI +USLnLV8fq77N+WxIqo20Az16biPxKL5jCq+NDqy0nF/J7ITBhMMCoYhPVxUhfjZ5 +7XLrSTjTVq7Mi1c5IQ== +-----END CERTIFICATE----- diff --git a/src/main/resources/mock-idp/certs/openssl_command.txt b/src/main/resources/mock-idp/certs/openssl_command.txt new file mode 100644 index 0000000..5e94909 --- /dev/null +++ b/src/main/resources/mock-idp/certs/openssl_command.txt @@ -0,0 +1,3 @@ +To create the idp-private.key and idp-public.cert: + +> openssl req -x509 -newkey rsa:2048 -keyout idp-private.key -out idp-public.cert -days 365 -nodes -subj "/CN=localhost" \ No newline at end of file diff --git a/src/main/resources/mock-idp/idp.js b/src/main/resources/mock-idp/idp.js new file mode 100644 index 0000000..1501495 --- /dev/null +++ b/src/main/resources/mock-idp/idp.js @@ -0,0 +1,315 @@ +const { SignedXml } = require('xml-crypto'); +const { DOMParser } = require('@xmldom/xmldom'); +const crypto = require('crypto'); +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const bodyParser = require('body-parser'); +const zlib = require('zlib'); +const xml2js = require('xml2js'); + +const app = express(); +const port = 3000; + +// Enable body parsing +app.use(bodyParser.urlencoded({ extended: true })); + +let storedSamlRequest = null; +let storedRelayState = null; +let storedQueryParameters = null; + +// Step 1: Receive SAMLRequest and render login form +app.get('/saml/sso', async (req, res) => { + storedQueryParameters = req.query; + console.log('Received query parameters:', req.query); // 👈 log all query parameters + + const samlRequest = req.query.SAMLRequest; + const relayState = req.query.RelayState || ''; + + if (!samlRequest) { + return res.status(400).send('Missing SAMLRequest'); + } + + // Store temporarily + storedSamlRequest = samlRequest; + storedRelayState = relayState; + + // Show simple login form + res.send(createLoginForm()); +}); + +// Step 2: Handle login form and send SAMLResponse +app.post('/saml/login', (req, res) => { + const { username, email, orgCode, securityGroup, roles } = req.body; + if (!storedSamlRequest) { + return res.status(400).send('No SAMLRequest stored'); + } + + // Get the issuer URL (the IDP ID [the application.yml = resilient.security.saml2.relyingparty.registration.mock-idp.assertingparty.entity-id]) + const issuerUrlDefault = 'http://localhost:3000/saml/metadata'; + const issuerUrl = storedQueryParameters['issuerUrl'] ? storedQueryParameters['issuerUrl'] : issuerUrlDefault; + + // Get the base url of ServiceProvider (the server APP) + const serviceProviderUrlDefault = 'https://localhost:8443'; // Or 'http://localhost:8081' + const serviceProviderUrl = storedQueryParameters['spUrl'] ? storedQueryParameters['spUrl'] : serviceProviderUrlDefault; + + // Build ACS (Assertion Consumer Service) URL — this is the Spring Boot app’s endpoint where the SAML Response is posted back after authentication. + const acsUrlPath = '/login/saml2/sso/mock-idp'; + const acsUrl = serviceProviderUrl + acsUrlPath; + + console.log('ServiceProvider URL :', serviceProviderUrl); + console.log('ServiceProvider ACS :', acsUrl); + + const samlResponse = createFakeSamlResponse(username, email, orgCode, securityGroup, roles, serviceProviderUrl, issuerUrl); + const relayState = storedRelayState; + + res.send(` + + +
+ + +
+ + + `); +}); + +function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles, serviceProviderUrl, issuerUrl) { + const issuer = issuerUrl; + // const audience = "https://localhost:8443/saml2/service-provider-metadata/mock-idp"; + const audience = serviceProviderUrl + "/saml2/service-provider-metadata/mock-idp"; + const now = new Date().toISOString(); + const fiveMinutesLater = new Date(Date.now() + 5 * 60000).toISOString(); + const assertionId = `_assertion_${crypto.randomUUID()}`; + const responseId = `_response_${crypto.randomUUID()}`; + + // const recipient = "https://localhost:8443/login/saml2/sso/mock-idp"; + const recipient = serviceProviderUrl + "/login/saml2/sso/mock-idp"; + // const destination = "https://localhost:8443/login/saml2/sso/mock-idp"; + const destination = serviceProviderUrl + "/login/saml2/sso/mock-idp"; + + console.log('SAML Recipient :', recipient); + console.log('SAML Destination :', destination); + console.log('SAML Issuer :', issuer); + + const assertion = ` + + ${issuer} + + ${nameId} + + + + + + + ${audience} + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + ${email} + + + ${orgCode} + + + ${securityGroupCode} + + + ${roles} + + + `; + + const privateKey = fs.readFileSync(path.join(__dirname, 'certs', 'idp-private.key'), 'utf8'); + const certificate = fs.readFileSync(path.join(__dirname, 'certs', 'idp-public.cert'), 'utf8'); + const certBase64 = certificate + .replace(/-----BEGIN CERTIFICATE-----/, '') + .replace(/-----END CERTIFICATE-----/, '') + .replace(/\r?\n|\r/g, ''); + + const doc = new DOMParser().parseFromString(assertion); + const sig = new SignedXml(); + sig.prefix = 'ds'; + sig.addReference("//*[local-name(.)='Assertion']", [ + "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + "http://www.w3.org/2001/10/xml-exc-c14n#", + ], "http://www.w3.org/2000/09/xmldsig#sha1"); + sig.signingKey = privateKey; + sig.keyInfoProvider = { + getKeyInfo: () => `${certBase64}` + }; + sig.computeSignature(assertion, { + prefix: 'ds', + location: { reference: "//*[local-name(.)='Issuer']", action: 'after' } + }); + + const signedAssertion = sig.getSignedXml(); + const samlResponse = `${issuer}${signedAssertion}`; + + const samlResponseToUse = samlResponse.trim().replace(/^\uFEFF/, ''); + return Buffer.from(samlResponseToUse, 'utf-8').toString('base64'); +} + +function createLoginFormOLD() { + const form = ` + + +

Mock Login

+
+ Username:
+ Email:
+ Org Code:
+ Security Group:
+ Roles:

+ +
+ + + `; + return form; +} + +function createLoginForm() { + const form = ` + + + + + +
+

Mock Login

+
+ + + + + + + + + + + + + + + + +
+
+ + + `; + + + return form; +} + +// Function to extract Issuer from SAMLRequest (returns a Promise) +function extractIssuerFromSAMLRequest(samlRequestBase64) { + return new Promise((resolve, reject) => { + const samlRequestBuffer = Buffer.from(samlRequestBase64, 'base64'); + + zlib.inflateRaw(samlRequestBuffer, (err, decoded) => { + if (err) { + return reject(new Error('Failed to inflate SAMLRequest')); + } + + const xml = decoded.toString('utf8'); + + xml2js.parseString(xml, { tagNameProcessors: [xml2js.processors.stripPrefix] }, (parseErr, result) => { + if (parseErr) { + return reject(new Error('Failed to parse SAMLRequest XML')); + } + + // Debugging: log the entire parsed result to see structure + // console.log("Parsed XML result:", result); + + // Extract Issuer from the first element of the array + const issuerObject = result?.AuthnRequest?.Issuer?.[0]; // Issuer is an array, so we access the first element + if (!issuerObject) { + return reject(new Error('Issuer not found in SAMLRequest')); + } + + // Access the text content of the Issuer object (it may be in the '#text' property) + const issuer = issuerObject._ || issuerObject['#text']; // Extract the actual string value + + if (!issuer) { + return reject(new Error('Issuer value is missing in the SAMLRequest')); + } + + resolve(issuer); // Return the issuer as a string + }); + }); + }); +} + +app.listen(port, () => { + console.log(`Mock IDP running at http://localhost:${port}`); +}); diff --git a/src/main/resources/mock-idp/package-lock.json b/src/main/resources/mock-idp/package-lock.json new file mode 100644 index 0000000..e42b770 --- /dev/null +++ b/src/main/resources/mock-idp/package-lock.json @@ -0,0 +1,1026 @@ +{ + "name": "mock-idp2", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mock-idp2", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^2.2.0", + "express": "^5.1.0", + "express-session": "^1.18.1", + "saml2-js": "^4.0.3", + "xml2js": "^0.6.2", + "zlib": "^1.0.5" + } + }, + "node_modules/@oozcitak/dom": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.8.tgz", + "integrity": "sha512-MoOnLBNsF+ok0HjpAvxYxR4piUhRDCEWK0ot3upwOOHYudJd30j6M+LNcE8RKpwfnclAX9T66nXXzkytd29XSw==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "dependencies": { + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@types/node": { + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "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==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/saml2-js": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/saml2-js/-/saml2-js-4.0.3.tgz", + "integrity": "sha512-lbQwkCuSgnogItacgaLM2YvVlCZxdcYSHtNfCgfGz+lKWfxBraP9i2iXzZjar2zIfBlpd+FMVH0ooOLkc5blPA==", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "async": "^3.2.0", + "debug": "^4.3.0", + "underscore": "^1.8.0", + "xml-crypto": "^3.2.1", + "xml-encryption": "^3.0.2", + "xmlbuilder2": "^2.4.0" + }, + "engines": { + "node": ">=10.x" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xml-crypto": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.1.tgz", + "integrity": "sha512-0GUNbPtQt+PLMsC5HoZRONX+K6NBJEqpXe/lsvrFj0EqfpGPpVfJKGE7a5jCg8s2+Wkrf/2U1G41kIH+zC9eyQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "xpath": "0.0.32" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml-encryption": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.1.0.tgz", + "integrity": "sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==", + "dependencies": { + "@xmldom/xmldom": "^0.8.5", + "escape-html": "^1.0.3", + "xpath": "0.0.32" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder2": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-2.4.1.tgz", + "integrity": "sha512-vliUplZsk5vJnhxXN/mRcij/AE24NObTUm/Zo4vkLusgayO6s3Et5zLEA14XZnY1c3hX5o1ToR0m0BJOPy0UvQ==", + "dependencies": { + "@oozcitak/dom": "1.15.8", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "@types/node": "*", + "js-yaml": "3.14.0" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/xpath": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", + "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/zlib": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", + "integrity": "sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w==", + "hasInstallScript": true, + "engines": { + "node": ">=0.2.0" + } + } + } +} diff --git a/src/main/resources/mock-idp/package.json b/src/main/resources/mock-idp/package.json new file mode 100644 index 0000000..33703a9 --- /dev/null +++ b/src/main/resources/mock-idp/package.json @@ -0,0 +1,20 @@ +{ + "name": "mock-idp2", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "body-parser": "^2.2.0", + "express": "^5.1.0", + "express-session": "^1.18.1", + "saml2-js": "^4.0.3", + "xml2js": "^0.6.2", + "zlib": "^1.0.5" + } +} diff --git a/src/main/resources/mock-idp/readme b/src/main/resources/mock-idp/readme new file mode 100644 index 0000000..edd4944 --- /dev/null +++ b/src/main/resources/mock-idp/readme @@ -0,0 +1,14 @@ +/* ***************************************************** */ +/* To run : > node idp.js */ +/* > Mock IDP running at http://localhost:3000 */ +/* Starts an IDP server, allowing SAMLv2 auth testing */ +/* ***************************************************** */ + +mock-idp\certs + Generated selfsigned cert for testing + * idp-private.key - Used for signing the SAML XML Response + * idp-public.cert - Included in the SAML XML Response + * openssl_command.txt - A file containing the command used to generate this certificate files + +idp.js + The mock idp developed in JS diff --git a/src/main/resources/saml/idp-public.cert b/src/main/resources/saml/idp-public.cert new file mode 100644 index 0000000..4cec70a --- /dev/null +++ b/src/main/resources/saml/idp-public.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUJGNdh9cxePuEzWTUlEuSOkwfwyUwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDQyNDA5MjI1MFoXDTI2MDQy +NDA5MjI1MFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8sZG9lKz8DcoeCxXTYZrz/8DVDZ8pg32W1LIoE9ypbMN +XzNeVdtYPPQuqahUtnz/EfWxwyuUPcRnY9RKXZh2c627mN8eDGnSE4blIE7ka3TZ +f083kQMv1HgB8FiYbXPl0KJocGaebCznJshHvDIn6KcslXFRp+PMA1xPnWtlcQbI +nROHxsjWDvlZy/9+59RXVT0fBYi4FlmKthG1k+VYOzeiUWleVSFD3KZx6yExuuPl +6PcEGAsfD6pnBHtERyv/CeEr7DZFagA0gpAoOMLIOQCu8W3F8z3MXcJp4pC1k9Ry +cujIjwqq5ilva3L+K5cTau3rVOVXhDC6+EEnv8IJpwIDAQABo1MwUTAdBgNVHQ4E +FgQURDfHxj6ACRLLPcVRTgvlwhJSnyAwHwYDVR0jBBgwFoAURDfHxj6ACRLLPcVR +TgvlwhJSnyAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABioO +OCok/9JigKTupTEnJLxgueSIqk0lWXa7E6zwHLJh+DUUSlS1kECL2+s+HlGz7oCj +uiemCLMssTd0ZOn95GN5b4IbEl8r+12hVaREKDC7qlydkFnKKomvEVQJUbAKPgqv +EhNmJsp9TPKQzCdwTz00g0mmZCtc4cJoXEiOR+TKf/AwxXbh8/++9big1hIAnpHC +zQgfebBg+I+1FPuUsqC6+zp4DrAflpQ2q0/4SNGyaPumf2K3ZjxLd9MPi+acd/HI +USLnLV8fq77N+WxIqo20Az16biPxKL5jCq+NDqy0nF/J7ITBhMMCoYhPVxUhfjZ5 +7XLrSTjTVq7Mi1c5IQ== +-----END CERTIFICATE----- diff --git a/src/main/resources/saml/openssl_command.txt b/src/main/resources/saml/openssl_command.txt new file mode 100644 index 0000000..af1a25f --- /dev/null +++ b/src/main/resources/saml/openssl_command.txt @@ -0,0 +1,23 @@ +This files were created to be used in configuration of SAML. + +spring: + security: + saml2: # ADDED to support SAMLv2 authentication to IDP. + relyingparty: + registration: + mock-idp: + signing: + credentials: + - private-key-location: classpath:saml/private.key + certificate-location: classpath:saml/public.cert + +private.key and public.cert will be used to sign requests to the relyingparty. +Also, the public.cert will be included in the generated metadata: http://localhost:8081/saml2/service-provider-metadata/mock-idp + +To generate private.key and public.cert : + +> openssl req -x509 -newkey rsa:2048 -keyout private.key -out public.cert -days 365 -nodes -subj "/CN=localhost" + + +The file idp-public.cert, was copied from the \certs + is a project for testing SMLv2 auth. Look in src\main\mock-idp \ No newline at end of file diff --git a/src/main/resources/saml/private.key b/src/main/resources/saml/private.key new file mode 100644 index 0000000..2dc6ae3 --- /dev/null +++ b/src/main/resources/saml/private.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCPJzvhW0+ZJfIY +AWdNJZ2kmasJZaZQYcoc1XaeMBqKN1WZd6FeYg7ydUEmT/jWjMTjwC4XEQ8UBbG1 +HRtygP2gzLsAgtAdPVYNRmjp1ZkuzQcAf8n5JYVtD7hnDMh1qRc6PQyu7DuzTKV7 +v/8accC/dog/v/pbzD8UNYrVekLkr3ZnMVuwalNP4LtyQkIkjgpf6yAPc/33dTVB +0ibhdmIukX/XRHi0e+SHDy/ZxW6cPyz3eZI/mJEZXkYqRxbThL6Q9cukuk+6Xr+j +NDLniUb9aG72ADgTb7t74xRk1I63RA8JmlPnW2pd5XOv1G9rhRC7glJyPIAMJPJ9 +fW92v8bPAgMBAAECggEAPvALciWQpe8adLZtQexXVk5fdca0q+OKlkm9mIfiq0Dv +yATiNBpdovitV5hSeZWzXMSaHvcXj4B/ZHbzIdEXcXKBhxR8Rla4tiZuKtyHuB1Z +Rj1owbre4hfGxbvHiH96oFbF9EQVbVu15+X+VRSNH0CQX6LagjCWzR5LN1dFAMCB +oc26uMJQJJ2PpEVJzN2trsnGGEh8B5cCGPQkQm/32kHZu01iDfazVpeYkC1tTfga +BaCNwIVthCI8QSt955EwAp+RJqFJEZ1RTHTbChr0e4HfZjaR2eqkRF2rKepNfkYH +OsoyQmUx6fs1RiDyg/yTQQdHXe9t/63K2l5xrz/UjQKBgQC6AydTk1R7P0rPCsP1 +406q8mebdW90Co0qOV7aqLltKAAl/Zzn9utkxjIdFoX0w2HzKDSDtZjn131lOnoZ +m6y3GGitvPsQlfKjMM4BgsB3wIaY1QBc+FHflrjSAy2i9VoGECfFbWpcdj2eSPCe +ZnWvRRzBehh8gg8/xZpBxH/iYwKBgQDFA9z/znbO7W+UQqqy2rNDwbFENqnhdiTb +fJttmvPIPX3IUAiZ5ulkeGDNIFDGUaFvp0rR0ffA4HfxMlTxSRIN/C6jf6icbaa2 +CgLdv7FlwjrXCfB9OuSWHnIJ6fGqG4nl2s4C0iv5nXVLhpj3QXmzusul8/9tV5g/ +/i0gJlm/pQKBgA12/PnHyG3Dfx7NoLHD6oXlViqirZawzNcpn/wkCogkRR565UjF +TvTUg/wnDTtSiVyYpHSESLU/nAwf14bTBgWa9rjc3S+uKwo1FjR1yojgnt2hTY1t +TD/rISXSpRMOv52AveBEs1ZOGNfEX+aXdB1j21kGBY+9umzbWPc5yigRAoGBAL2V +dyAuNMPxVdLsyEhuDd1g0U1b8OPGBbe0U/kICBkjmxuccHpw6xzEfhpGQ5Rjzx6b +dHMuQuQ23RScI4NAloAgsVyiJsXI+ojoX1O1+GZ8npR5uxM3Deqrh3+kwOm5sFwg +zueGCc0dSVcRQbeFKTDbysZ6Ysx8Y0UJObFbVjgFAoGBAIX+yEGkjXvU89EcgHuD ++s8PgfZcPaiLWx7VoRdnEuPQjcVOK1vp8n8uqifCJS2FS6etwAnz96aO6fQl2UkH +ojDYUrYuJEXNJgWXJE5mmb5rJleROb5g1XOnYYROtRDSQszH1TX0/7f8VsJM9OXA +AFrlZCFY9xAzt7q68W3eP+dp +-----END PRIVATE KEY----- diff --git a/src/main/resources/saml/public.cert b/src/main/resources/saml/public.cert new file mode 100644 index 0000000..c050910 --- /dev/null +++ b/src/main/resources/saml/public.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUQqNXwb0T1caRN7nKONTJbR51bvgwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDQyNDA5MjUxMVoXDTI2MDQy +NDA5MjUxMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAjyc74VtPmSXyGAFnTSWdpJmrCWWmUGHKHNV2njAaijdV +mXehXmIO8nVBJk/41ozE48AuFxEPFAWxtR0bcoD9oMy7AILQHT1WDUZo6dWZLs0H +AH/J+SWFbQ+4ZwzIdakXOj0Mruw7s0yle7//GnHAv3aIP7/6W8w/FDWK1XpC5K92 +ZzFbsGpTT+C7ckJCJI4KX+sgD3P993U1QdIm4XZiLpF/10R4tHvkhw8v2cVunD8s +93mSP5iRGV5GKkcW04S+kPXLpLpPul6/ozQy54lG/Whu9gA4E2+7e+MUZNSOt0QP +CZpT51tqXeVzr9Rva4UQu4JScjyADCTyfX1vdr/GzwIDAQABo1MwUTAdBgNVHQ4E +FgQUq28aH7crQfcICm94KDRMRb9+WD4wHwYDVR0jBBgwFoAUq28aH7crQfcICm94 +KDRMRb9+WD4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEZB3 +csWNh96nkRgyoVVnGRw77ZkRpLzRNyEfUVBjVJ6Fn1tSzbgJIST8ZkYJpbONwI5u +JDH39A6tIbxFeK3uQbpZJNi+mMUJtBDOlQ4LY0k/zTE66N2+OntIvpQB0pGqAqZ4 +9zTG1Of6w/G0WzIWk4jivhFrakHTj+WxwUq/u1KKwPqptdkP530noWxdNC+lEzWH +1zhq/xojgsyGbYeG12vlPCUctDcV1KFvkpctD+QgN5gGus3NLj5qw9oWh6a3RSZf +8t6kZz+2bhTQx7gvgKb8iRbkqZLPOJrJMhVGBKK/p7JMkYUI2i/UGEGGzh8tnqaA +tfMYiOFtwh/119Gj0A== +-----END CERTIFICATE----- diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..31ee9d7 --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,94 @@ + + + + + + Your request cannot be processed + + + +
+

Your request cannot be processed :(

+ +

Sorry, an error has occurred.

+ + Status:  ()
+ + Message: 
+
+
+ + diff --git a/src/main/resources/templates/mail/activationEmail.html b/src/main/resources/templates/mail/activationEmail.html new file mode 100644 index 0000000..6be1e4f --- /dev/null +++ b/src/main/resources/templates/mail/activationEmail.html @@ -0,0 +1,20 @@ + + + + JHipster activation + + + + +

Dear

+

Your JHipster account has been created, please click on the URL below to activate it:

+

+ Activation link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/src/main/resources/templates/mail/creationEmail.html b/src/main/resources/templates/mail/creationEmail.html new file mode 100644 index 0000000..ab46621 --- /dev/null +++ b/src/main/resources/templates/mail/creationEmail.html @@ -0,0 +1,20 @@ + + + + JHipster creation + + + + +

Dear

+

Your JHipster account has been created, please click on the URL below to access it:

+

+ Login link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/src/main/resources/templates/mail/passwordResetEmail.html b/src/main/resources/templates/mail/passwordResetEmail.html new file mode 100644 index 0000000..eb74196 --- /dev/null +++ b/src/main/resources/templates/mail/passwordResetEmail.html @@ -0,0 +1,22 @@ + + + + JHipster password reset + + + + +

Dear

+

+ For your JHipster account a password reset was requested, please click on the URL below to reset it: +

+

+ Login link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/src/main/webapp/404.html b/src/main/webapp/404.html new file mode 100644 index 0000000..2ef8c49 --- /dev/null +++ b/src/main/webapp/404.html @@ -0,0 +1,58 @@ + + + + + Page Not Found + + + + + +

Page Not Found

+

Sorry, but the page you were trying to view does not exist.

+ + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f1611b5 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + + + + html + text/html;charset=utf-8 + + + diff --git a/src/main/webapp/app/account/account.route.ts b/src/main/webapp/app/account/account.route.ts new file mode 100644 index 0000000..1c423ec --- /dev/null +++ b/src/main/webapp/app/account/account.route.ts @@ -0,0 +1,21 @@ +import { Routes } from '@angular/router'; + +import activateRoute from './activate/activate.route'; +import passwordRoute from './password/password.route'; +import passwordResetFinishRoute from './password-reset/finish/password-reset-finish.route'; +import passwordResetInitRoute from './password-reset/init/password-reset-init.route'; +import registerRoute from './register/register.route'; +import sessionsRoute from './sessions/sessions.route'; +import settingsRoute from './settings/settings.route'; + +const accountRoutes: Routes = [ + activateRoute, + passwordRoute, + passwordResetFinishRoute, + passwordResetInitRoute, + /* registerRoute, DISABLED Users are created by admin, not requested */ + sessionsRoute, + settingsRoute, +]; + +export default accountRoutes; diff --git a/src/main/webapp/app/account/activate/activate.component.html b/src/main/webapp/app/account/activate/activate.component.html new file mode 100644 index 0000000..7b041d8 --- /dev/null +++ b/src/main/webapp/app/account/activate/activate.component.html @@ -0,0 +1,18 @@ +
+
+
+

Ativação

+ @if (success()) { +
+ Utilizador ativado com sucesso. Por favor + entrar. +
+ } + @if (error()) { +
+ O utilizador não pode ser ativado. Por favor utilize o formulário de registo para criar uma nova conta. +
+ } +
+
+
diff --git a/src/main/webapp/app/account/activate/activate.component.spec.ts b/src/main/webapp/app/account/activate/activate.component.spec.ts new file mode 100644 index 0000000..c99a0fa --- /dev/null +++ b/src/main/webapp/app/account/activate/activate.component.spec.ts @@ -0,0 +1,68 @@ +import { TestBed, waitForAsync, tick, fakeAsync, inject } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, throwError } from 'rxjs'; + +import { ActivateService } from './activate.service'; +import ActivateComponent from './activate.component'; + +describe('ActivateComponent', () => { + let comp: ActivateComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, ActivateComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { queryParams: of({ key: 'ABC123' }) }, + }, + ], + }) + .overrideTemplate(ActivateComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + const fixture = TestBed.createComponent(ActivateComponent); + comp = fixture.componentInstance; + }); + + it('calls activate.get with the key from params', inject( + [ActivateService], + fakeAsync((service: ActivateService) => { + jest.spyOn(service, 'get').mockReturnValue(of()); + + comp.ngOnInit(); + tick(); + + expect(service.get).toHaveBeenCalledWith('ABC123'); + }), + )); + + it('should set set success to true upon successful activation', inject( + [ActivateService], + fakeAsync((service: ActivateService) => { + jest.spyOn(service, 'get').mockReturnValue(of({})); + + comp.ngOnInit(); + tick(); + + expect(comp.error()).toBe(false); + expect(comp.success()).toBe(true); + }), + )); + + it('should set set error to true upon activation failure', inject( + [ActivateService], + fakeAsync((service: ActivateService) => { + jest.spyOn(service, 'get').mockReturnValue(throwError('ERROR')); + + comp.ngOnInit(); + tick(); + + expect(comp.error()).toBe(true); + expect(comp.success()).toBe(false); + }), + )); +}); diff --git a/src/main/webapp/app/account/activate/activate.component.ts b/src/main/webapp/app/account/activate/activate.component.ts new file mode 100644 index 0000000..ab333c0 --- /dev/null +++ b/src/main/webapp/app/account/activate/activate.component.ts @@ -0,0 +1,27 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import { mergeMap } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { ActivateService } from './activate.service'; + +@Component({ + standalone: true, + selector: 'jhi-activate', + imports: [SharedModule, RouterModule], + templateUrl: './activate.component.html', +}) +export default class ActivateComponent implements OnInit { + error = signal(false); + success = signal(false); + + private activateService = inject(ActivateService); + private route = inject(ActivatedRoute); + + ngOnInit(): void { + this.route.queryParams.pipe(mergeMap(params => this.activateService.get(params.key))).subscribe({ + next: () => this.success.set(true), + error: () => this.error.set(true), + }); + } +} diff --git a/src/main/webapp/app/account/activate/activate.route.ts b/src/main/webapp/app/account/activate/activate.route.ts new file mode 100644 index 0000000..5f60255 --- /dev/null +++ b/src/main/webapp/app/account/activate/activate.route.ts @@ -0,0 +1,11 @@ +import { Route } from '@angular/router'; + +import ActivateComponent from './activate.component'; + +const activateRoute: Route = { + path: 'activate', + component: ActivateComponent, + title: 'activate.title', +}; + +export default activateRoute; diff --git a/src/main/webapp/app/account/activate/activate.service.spec.ts b/src/main/webapp/app/account/activate/activate.service.spec.ts new file mode 100644 index 0000000..e33b3bf --- /dev/null +++ b/src/main/webapp/app/account/activate/activate.service.spec.ts @@ -0,0 +1,47 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ActivateService } from './activate.service'; + +describe('ActivateService Service', () => { + let service: ActivateService; + let httpMock: HttpTestingController; + let applicationConfigService: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(ActivateService); + applicationConfigService = TestBed.inject(ApplicationConfigService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should call api/activate endpoint with correct values', () => { + // GIVEN + let expectedResult; + const key = 'key'; + const value = true; + + // WHEN + service.get(key).subscribe(received => { + expectedResult = received; + }); + const testRequest = httpMock.expectOne({ + method: 'GET', + url: applicationConfigService.getEndpointFor(`api/activate?key=${key}`), + }); + testRequest.flush(value); + + // THEN + expect(expectedResult).toEqual(value); + }); + }); +}); diff --git a/src/main/webapp/app/account/activate/activate.service.ts b/src/main/webapp/app/account/activate/activate.service.ts new file mode 100644 index 0000000..5831558 --- /dev/null +++ b/src/main/webapp/app/account/activate/activate.service.ts @@ -0,0 +1,17 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; + +@Injectable({ providedIn: 'root' }) +export class ActivateService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + get(key: string): Observable<{}> { + return this.http.get(this.applicationConfigService.getEndpointFor('api/activate'), { + params: new HttpParams().set('key', key), + }); + } +} diff --git a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.html b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.html new file mode 100644 index 0000000..06cb85e --- /dev/null +++ b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.html @@ -0,0 +1,135 @@ +
+
+
+

Reposição da palavra-passe

+ + @if (initialized() && !key()) { +
Chave de reposição em falta.
+ } + + @if (key() && !success()) { +
+ Escolha uma nova palavra-passe +
+ } + + @if (error()) { +
+ A sua palavra-passe não pode ser reposta. O pedido de reposição da palavra-passe é válido apenas por 24 horas. +
+ } + + @if (success()) { +
+ A sua palavra-passe foi reposta. Por favor + entrar. +
+ } + + @if (doNotMatch()) { +
+ A palavra-passe e a sua confirmação devem ser iguais! +
+ } + + @if (key() && !success()) { +
+
+
+ + + + @if ( + passwordForm.get('newPassword')!.invalid && + (passwordForm.get('newPassword')!.dirty || passwordForm.get('newPassword')!.touched) + ) { +
+ @if (passwordForm.get('newPassword')?.errors?.required) { + A palavra-passe é obrigatória. + } + + @if (passwordForm.get('newPassword')?.errors?.minlength) { + A palavra-passe deve ter pelo menos 4 caracteres + } + + @if (passwordForm.get('newPassword')?.errors?.maxlength) { + A palavra-passe não pode ter mais de 50 caracteres + } +
+ } + + +
+ +
+ + + + @if ( + passwordForm.get('confirmPassword')!.invalid && + (passwordForm.get('confirmPassword')!.dirty || passwordForm.get('confirmPassword')!.touched) + ) { +
+ @if (passwordForm.get('confirmPassword')?.errors?.required) { + A confirmação da palavra-passe é obrigatória. + } + + @if (passwordForm.get('confirmPassword')?.errors?.minlength) { + A confirmação da palavra-passe deve ter pelo menos 4 caracteres + } + + @if (passwordForm.get('confirmPassword')?.errors?.maxlength) { + A confirmação da palavra-passe não pode ter mais de 50 caracteres + } +
+ } +
+ + +
+
+ } +
+
+
diff --git a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.spec.ts b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.spec.ts new file mode 100644 index 0000000..44f7ffd --- /dev/null +++ b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.spec.ts @@ -0,0 +1,97 @@ +import { ElementRef, signal } from '@angular/core'; +import { ComponentFixture, TestBed, inject, tick, fakeAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, throwError } from 'rxjs'; + +import PasswordResetFinishComponent from './password-reset-finish.component'; +import { PasswordResetFinishService } from './password-reset-finish.service'; + +describe('PasswordResetFinishComponent', () => { + let fixture: ComponentFixture; + let comp: PasswordResetFinishComponent; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PasswordResetFinishComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { queryParams: of({ key: 'XYZPDQ' }) }, + }, + ], + }) + .overrideTemplate(PasswordResetFinishComponent, '') + .createComponent(PasswordResetFinishComponent); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordResetFinishComponent); + comp = fixture.componentInstance; + comp.ngOnInit(); + }); + + it('should define its initial state', () => { + expect(comp.initialized()).toBe(true); + expect(comp.key()).toEqual('XYZPDQ'); + }); + + it('sets focus after the view has been initialized', () => { + const node = { + focus: jest.fn(), + }; + comp.newPassword = signal(new ElementRef(node)); + + comp.ngAfterViewInit(); + + expect(node.focus).toHaveBeenCalled(); + }); + + it('should ensure the two passwords entered match', () => { + comp.passwordForm.patchValue({ + newPassword: 'password', + confirmPassword: 'non-matching', + }); + + comp.finishReset(); + + expect(comp.doNotMatch()).toBe(true); + }); + + it('should update success to true after resetting password', inject( + [PasswordResetFinishService], + fakeAsync((service: PasswordResetFinishService) => { + jest.spyOn(service, 'save').mockReturnValue(of({})); + comp.passwordForm.patchValue({ + newPassword: 'password', + confirmPassword: 'password', + }); + + comp.finishReset(); + tick(); + + expect(service.save).toHaveBeenCalledWith('XYZPDQ', 'password'); + expect(comp.success()).toBe(true); + }), + )); + + it('should notify of generic error', inject( + [PasswordResetFinishService], + fakeAsync((service: PasswordResetFinishService) => { + jest.spyOn(service, 'save').mockReturnValue(throwError('ERROR')); + comp.passwordForm.patchValue({ + newPassword: 'password', + confirmPassword: 'password', + }); + + comp.finishReset(); + tick(); + + expect(service.save).toHaveBeenCalledWith('XYZPDQ', 'password'); + expect(comp.success()).toBe(false); + expect(comp.error()).toBe(true); + }), + )); +}); diff --git a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts new file mode 100644 index 0000000..061db1f --- /dev/null +++ b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts @@ -0,0 +1,66 @@ +import { Component, inject, OnInit, AfterViewInit, ElementRef, signal, viewChild } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import PasswordStrengthBarComponent from 'app/account/password/password-strength-bar/password-strength-bar.component'; +import SharedModule from 'app/shared/shared.module'; + +import { PasswordResetFinishService } from './password-reset-finish.service'; + +@Component({ + standalone: true, + selector: 'jhi-password-reset-finish', + imports: [SharedModule, RouterModule, FormsModule, ReactiveFormsModule, PasswordStrengthBarComponent], + templateUrl: './password-reset-finish.component.html', +}) +export default class PasswordResetFinishComponent implements OnInit, AfterViewInit { + newPassword = viewChild.required('newPassword'); + + initialized = signal(false); + doNotMatch = signal(false); + error = signal(false); + success = signal(false); + key = signal(''); + + passwordForm = new FormGroup({ + newPassword: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)], + }), + confirmPassword: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)], + }), + }); + + private passwordResetFinishService = inject(PasswordResetFinishService); + private route = inject(ActivatedRoute); + + ngOnInit(): void { + this.route.queryParams.subscribe(params => { + if (params['key']) { + this.key.set(params['key']); + } + this.initialized.set(true); + }); + } + + ngAfterViewInit(): void { + this.newPassword().nativeElement.focus(); + } + + finishReset(): void { + this.doNotMatch.set(false); + this.error.set(false); + + const { newPassword, confirmPassword } = this.passwordForm.getRawValue(); + + if (newPassword !== confirmPassword) { + this.doNotMatch.set(true); + } else { + this.passwordResetFinishService.save(this.key(), newPassword).subscribe({ + next: () => this.success.set(true), + error: () => this.error.set(true), + }); + } + } +} diff --git a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.route.ts b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.route.ts new file mode 100644 index 0000000..2963d15 --- /dev/null +++ b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.route.ts @@ -0,0 +1,11 @@ +import { Route } from '@angular/router'; + +import PasswordResetFinishComponent from './password-reset-finish.component'; + +const passwordResetFinishRoute: Route = { + path: 'reset/finish', + component: PasswordResetFinishComponent, + title: 'global.menu.account.password', +}; + +export default passwordResetFinishRoute; diff --git a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.spec.ts b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.spec.ts new file mode 100644 index 0000000..da6a825 --- /dev/null +++ b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { PasswordResetFinishService } from './password-reset-finish.service'; + +describe('PasswordResetFinish Service', () => { + let service: PasswordResetFinishService; + let httpMock: HttpTestingController; + let applicationConfigService: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(PasswordResetFinishService); + applicationConfigService = TestBed.inject(ApplicationConfigService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should call reset-password/finish endpoint with correct values', () => { + // GIVEN + const key = 'abc'; + const newPassword = 'password'; + + // WHEN + service.save(key, newPassword).subscribe(); + + const testRequest = httpMock.expectOne({ + method: 'POST', + url: applicationConfigService.getEndpointFor('api/account/reset-password/finish'), + }); + + // THEN + expect(testRequest.request.body).toEqual({ key, newPassword }); + }); + }); +}); diff --git a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.ts b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.ts new file mode 100644 index 0000000..f3aa78b --- /dev/null +++ b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.service.ts @@ -0,0 +1,15 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; + +@Injectable({ providedIn: 'root' }) +export class PasswordResetFinishService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + save(key: string, newPassword: string): Observable<{}> { + return this.http.post(this.applicationConfigService.getEndpointFor('api/account/reset-password/finish'), { key, newPassword }); + } +} diff --git a/src/main/webapp/app/account/password-reset/init/password-reset-init.component.html b/src/main/webapp/app/account/password-reset/init/password-reset-init.component.html new file mode 100644 index 0000000..0bbb918 --- /dev/null +++ b/src/main/webapp/app/account/password-reset/init/password-reset-init.component.html @@ -0,0 +1,71 @@ +
+
+
+

Reponha a palavra-passe

+ + + + @if (!success()) { +
+ Introduza o endereço de email utilizado para registar +
+
+
+ + + + @if ( + resetRequestForm.get('email')!.invalid && (resetRequestForm.get('email')!.dirty || resetRequestForm.get('email')!.touched) + ) { +
+ @if (resetRequestForm.get('email')?.errors?.required) { + O email é obrigatório. + } + @if (resetRequestForm.get('email')?.errors?.email) { + Email inválido. + } + + @if (resetRequestForm.get('email')?.errors?.minlength) { + O email deve ter pelo menos 5 caracteres + } + + @if (resetRequestForm.get('email')?.errors?.maxlength) { + O email não pode ter mais de 50 caracteres + } +
+ } +
+ + +
+ } @else { +
+ Verifique o seu email para mais detalhes sobre como repor a palavra-passe. +
+ } +
+
+
diff --git a/src/main/webapp/app/account/password-reset/init/password-reset-init.component.spec.ts b/src/main/webapp/app/account/password-reset/init/password-reset-init.component.spec.ts new file mode 100644 index 0000000..c3b9e99 --- /dev/null +++ b/src/main/webapp/app/account/password-reset/init/password-reset-init.component.spec.ts @@ -0,0 +1,58 @@ +import { ElementRef, signal } from '@angular/core'; +import { ComponentFixture, TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { of, throwError } from 'rxjs'; + +import PasswordResetInitComponent from './password-reset-init.component'; +import { PasswordResetInitService } from './password-reset-init.service'; + +describe('PasswordResetInitComponent', () => { + let fixture: ComponentFixture; + let comp: PasswordResetInitComponent; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PasswordResetInitComponent], + providers: [FormBuilder], + }) + .overrideTemplate(PasswordResetInitComponent, '') + .createComponent(PasswordResetInitComponent); + comp = fixture.componentInstance; + }); + + it('sets focus after the view has been initialized', () => { + const node = { + focus: jest.fn(), + }; + comp.email = signal(new ElementRef(node)); + + comp.ngAfterViewInit(); + + expect(node.focus).toHaveBeenCalled(); + }); + + it('notifies of success upon successful requestReset', inject([PasswordResetInitService], (service: PasswordResetInitService) => { + jest.spyOn(service, 'save').mockReturnValue(of({})); + comp.resetRequestForm.patchValue({ + email: 'user@domain.com', + }); + + comp.requestReset(); + + expect(service.save).toHaveBeenCalledWith('user@domain.com'); + expect(comp.success()).toBe(true); + })); + + it('no notification of success upon error response', inject([PasswordResetInitService], (service: PasswordResetInitService) => { + const err = { status: 503, data: 'something else' }; + jest.spyOn(service, 'save').mockReturnValue(throwError(() => err)); + comp.resetRequestForm.patchValue({ + email: 'user@domain.com', + }); + comp.requestReset(); + + expect(service.save).toHaveBeenCalledWith('user@domain.com'); + expect(comp.success()).toBe(false); + })); +}); diff --git a/src/main/webapp/app/account/password-reset/init/password-reset-init.component.ts b/src/main/webapp/app/account/password-reset/init/password-reset-init.component.ts new file mode 100644 index 0000000..27c53b4 --- /dev/null +++ b/src/main/webapp/app/account/password-reset/init/password-reset-init.component.ts @@ -0,0 +1,35 @@ +import { Component, AfterViewInit, ElementRef, inject, signal, viewChild } from '@angular/core'; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import SharedModule from 'app/shared/shared.module'; + +import { PasswordResetInitService } from './password-reset-init.service'; + +@Component({ + standalone: true, + selector: 'jhi-password-reset-init', + imports: [SharedModule, FormsModule, ReactiveFormsModule], + templateUrl: './password-reset-init.component.html', +}) +export default class PasswordResetInitComponent implements AfterViewInit { + email = viewChild.required('email'); + + success = signal(false); + resetRequestForm; + + private passwordResetInitService = inject(PasswordResetInitService); + private fb = inject(FormBuilder); + + constructor() { + this.resetRequestForm = this.fb.group({ + email: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email]], + }); + } + + ngAfterViewInit(): void { + this.email().nativeElement.focus(); + } + + requestReset(): void { + this.passwordResetInitService.save(this.resetRequestForm.get(['email'])!.value).subscribe(() => this.success.set(true)); + } +} diff --git a/src/main/webapp/app/account/password-reset/init/password-reset-init.route.ts b/src/main/webapp/app/account/password-reset/init/password-reset-init.route.ts new file mode 100644 index 0000000..654634b --- /dev/null +++ b/src/main/webapp/app/account/password-reset/init/password-reset-init.route.ts @@ -0,0 +1,11 @@ +import { Route } from '@angular/router'; + +import PasswordResetInitComponent from './password-reset-init.component'; + +const passwordResetInitRoute: Route = { + path: 'reset/request', + component: PasswordResetInitComponent, + title: 'global.menu.account.password', +}; + +export default passwordResetInitRoute; diff --git a/src/main/webapp/app/account/password-reset/init/password-reset-init.service.spec.ts b/src/main/webapp/app/account/password-reset/init/password-reset-init.service.spec.ts new file mode 100644 index 0000000..3a05b2d --- /dev/null +++ b/src/main/webapp/app/account/password-reset/init/password-reset-init.service.spec.ts @@ -0,0 +1,43 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { PasswordResetInitService } from './password-reset-init.service'; + +describe('PasswordResetInit Service', () => { + let service: PasswordResetInitService; + let httpMock: HttpTestingController; + let applicationConfigService: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(PasswordResetInitService); + applicationConfigService = TestBed.inject(ApplicationConfigService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should call reset-password/init endpoint with correct values', () => { + // GIVEN + const mail = 'test@test.com'; + + // WHEN + service.save(mail).subscribe(); + + const testRequest = httpMock.expectOne({ + method: 'POST', + url: applicationConfigService.getEndpointFor('api/account/reset-password/init'), + }); + + // THEN + expect(testRequest.request.body).toEqual(mail); + }); + }); +}); diff --git a/src/main/webapp/app/account/password-reset/init/password-reset-init.service.ts b/src/main/webapp/app/account/password-reset/init/password-reset-init.service.ts new file mode 100644 index 0000000..9c0cddd --- /dev/null +++ b/src/main/webapp/app/account/password-reset/init/password-reset-init.service.ts @@ -0,0 +1,15 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; + +@Injectable({ providedIn: 'root' }) +export class PasswordResetInitService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + save(mail: string): Observable<{}> { + return this.http.post(this.applicationConfigService.getEndpointFor('api/account/reset-password/init'), mail); + } +} diff --git a/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.html b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.html new file mode 100644 index 0000000..b4e3b02 --- /dev/null +++ b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.html @@ -0,0 +1,10 @@ +
+ Nível de dificuldade da palavra-passe: +
    +
  • +
  • +
  • +
  • +
  • +
+
diff --git a/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.scss b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.scss new file mode 100644 index 0000000..67ce468 --- /dev/null +++ b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.scss @@ -0,0 +1,23 @@ +/* ========================================================================== +start Password strength bar style +========================================================================== */ +ul#strength { + display: inline; + list-style: none; + margin: 0; + margin-left: 15px; + padding: 0; + vertical-align: 2px; +} + +.point { + background: #ddd; + border-radius: 2px; + display: inline-block; + height: 5px; + margin-right: 1px; + width: 20px; + &:last-child { + margin: 0 !important; + } +} diff --git a/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.spec.ts b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.spec.ts new file mode 100644 index 0000000..9fd533a --- /dev/null +++ b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import PasswordStrengthBarComponent from './password-strength-bar.component'; + +describe('PasswordStrengthBarComponent', () => { + let comp: PasswordStrengthBarComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [PasswordStrengthBarComponent], + }) + .overrideTemplate(PasswordStrengthBarComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordStrengthBarComponent); + comp = fixture.componentInstance; + }); + + describe('PasswordStrengthBarComponents', () => { + it('should initialize with default values', () => { + expect(comp.measureStrength('')).toBe(0); + expect(comp.colors).toEqual(['#F00', '#F90', '#FF0', '#9F0', '#0F0']); + expect(comp.getColor(0).idx).toBe(1); + expect(comp.getColor(0).color).toBe(comp.colors[0]); + }); + + it('should increase strength upon password value change', () => { + expect(comp.measureStrength('')).toBe(0); + expect(comp.measureStrength('aa')).toBeGreaterThanOrEqual(comp.measureStrength('')); + expect(comp.measureStrength('aa^6')).toBeGreaterThanOrEqual(comp.measureStrength('aa')); + expect(comp.measureStrength('Aa090(**)')).toBeGreaterThanOrEqual(comp.measureStrength('aa^6')); + expect(comp.measureStrength('Aa090(**)+-07365')).toBeGreaterThanOrEqual(comp.measureStrength('Aa090(**)')); + }); + + it('should change the color based on strength', () => { + expect(comp.getColor(0).color).toBe(comp.colors[0]); + expect(comp.getColor(11).color).toBe(comp.colors[1]); + expect(comp.getColor(22).color).toBe(comp.colors[2]); + expect(comp.getColor(33).color).toBe(comp.colors[3]); + expect(comp.getColor(44).color).toBe(comp.colors[4]); + }); + }); +}); diff --git a/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.ts b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.ts new file mode 100644 index 0000000..9ffd735 --- /dev/null +++ b/src/main/webapp/app/account/password/password-strength-bar/password-strength-bar.component.ts @@ -0,0 +1,77 @@ +import { Component, ElementRef, inject, Input, Renderer2 } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; + +@Component({ + standalone: true, + selector: 'jhi-password-strength-bar', + imports: [SharedModule], + templateUrl: './password-strength-bar.component.html', + styleUrl: './password-strength-bar.component.scss', +}) +export default class PasswordStrengthBarComponent { + colors = ['#F00', '#F90', '#FF0', '#9F0', '#0F0']; + + private renderer = inject(Renderer2); + private elementRef = inject(ElementRef); + + measureStrength(p: string): number { + let force = 0; + const regex = /[$-/:-?{-~!"^_`[\]]/g; // " + const lowerLetters = /[a-z]+/.test(p); + const upperLetters = /[A-Z]+/.test(p); + const numbers = /\d+/.test(p); + const symbols = regex.test(p); + + const flags = [lowerLetters, upperLetters, numbers, symbols]; + const passedMatches = flags.filter((isMatchedFlag: boolean) => isMatchedFlag === true).length; + + force += 2 * p.length + (p.length >= 10 ? 1 : 0); + force += passedMatches * 10; + + // penalty (short password) + force = p.length <= 6 ? Math.min(force, 10) : force; + + // penalty (poor variety of characters) + force = passedMatches === 1 ? Math.min(force, 10) : force; + force = passedMatches === 2 ? Math.min(force, 20) : force; + force = passedMatches === 3 ? Math.min(force, 40) : force; + + return force; + } + + getColor(s: number): { idx: number; color: string } { + let idx = 0; + if (s > 10) { + if (s <= 20) { + idx = 1; + } else if (s <= 30) { + idx = 2; + } else if (s <= 40) { + idx = 3; + } else { + idx = 4; + } + } + return { idx: idx + 1, color: this.colors[idx] }; + } + + @Input() + set passwordToCheck(password: string) { + if (password) { + const c = this.getColor(this.measureStrength(password)); + const element = this.elementRef.nativeElement; + if (element.className) { + this.renderer.removeClass(element, element.className); + } + const lis = element.getElementsByTagName('li'); + for (let i = 0; i < lis.length; i++) { + if (i < c.idx) { + this.renderer.setStyle(lis[i], 'backgroundColor', c.color); + } else { + this.renderer.setStyle(lis[i], 'backgroundColor', '#DDD'); + } + } + } + } +} diff --git a/src/main/webapp/app/account/password/password.component.html b/src/main/webapp/app/account/password/password.component.html new file mode 100644 index 0000000..b328406 --- /dev/null +++ b/src/main/webapp/app/account/password/password.component.html @@ -0,0 +1,146 @@ +
+
+ @if (account$ | async; as account) { +
+

+ Palavra-passe para [{{ account.login }}] +

+ + @if (success()) { +
+ Palavra-passe alterada com sucesso! +
+ } + @if (error()) { +
+ Ocorreu um erro! A palavra-passe não pode ser alterada. +
+ } + @if (doNotMatch()) { +
+ A palavra-passe e a sua confirmação devem ser iguais! +
+ } + +
+
+ + + + @if ( + passwordForm.get('currentPassword')!.invalid && + (passwordForm.get('currentPassword')!.dirty || passwordForm.get('currentPassword')!.touched) + ) { +
+ @if (passwordForm.get('currentPassword')?.errors?.required) { + A palavra-passe é obrigatória. + } +
+ } +
+ +
+ + + + @if ( + passwordForm.get('newPassword')!.invalid && + (passwordForm.get('newPassword')!.dirty || passwordForm.get('newPassword')!.touched) + ) { +
+ @if (passwordForm.get('newPassword')?.errors?.required) { + A palavra-passe é obrigatória. + } + + @if (passwordForm.get('newPassword')?.errors?.minlength) { + A palavra-passe deve ter pelo menos 4 caracteres + } + + @if (passwordForm.get('newPassword')?.errors?.maxlength) { + A palavra-passe não pode ter mais de 50 caracteres + } +
+ } + + +
+ +
+ + + + @if ( + passwordForm.get('confirmPassword')!.invalid && + (passwordForm.get('confirmPassword')!.dirty || passwordForm.get('confirmPassword')!.touched) + ) { +
+ @if (passwordForm.get('confirmPassword')?.errors?.required) { + A confirmação da palavra-passe é obrigatória. + } + + @if (passwordForm.get('confirmPassword')?.errors?.minlength) { + A confirmação da palavra-passe deve ter pelo menos 4 caracteres + } + + @if (passwordForm.get('confirmPassword')?.errors?.maxlength) { + A confirmação da palavra-passe não pode ter mais de 50 caracteres + } +
+ } +
+ + +
+
+ } +
+
diff --git a/src/main/webapp/app/account/password/password.component.spec.ts b/src/main/webapp/app/account/password/password.component.spec.ts new file mode 100644 index 0000000..d82d46c --- /dev/null +++ b/src/main/webapp/app/account/password/password.component.spec.ts @@ -0,0 +1,103 @@ +jest.mock('app/core/auth/account.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { of, throwError } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; + +import PasswordComponent from './password.component'; +import { PasswordService } from './password.service'; + +describe('PasswordComponent', () => { + let comp: PasswordComponent; + let fixture: ComponentFixture; + let service: PasswordService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PasswordComponent], + providers: [FormBuilder, AccountService], + }) + .overrideTemplate(PasswordComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordComponent); + comp = fixture.componentInstance; + service = TestBed.inject(PasswordService); + }); + + it('should show error if passwords do not match', () => { + // GIVEN + comp.passwordForm.patchValue({ + newPassword: 'password1', + confirmPassword: 'password2', + }); + // WHEN + comp.changePassword(); + // THEN + expect(comp.doNotMatch()).toBe(true); + expect(comp.error()).toBe(false); + expect(comp.success()).toBe(false); + }); + + it('should call Auth.changePassword when passwords match', () => { + // GIVEN + const passwordValues = { + currentPassword: 'oldPassword', + newPassword: 'myPassword', + }; + + jest.spyOn(service, 'save').mockReturnValue(of(new HttpResponse({ body: true }))); + + comp.passwordForm.patchValue({ + currentPassword: passwordValues.currentPassword, + newPassword: passwordValues.newPassword, + confirmPassword: passwordValues.newPassword, + }); + + // WHEN + comp.changePassword(); + + // THEN + expect(service.save).toHaveBeenCalledWith(passwordValues.newPassword, passwordValues.currentPassword); + }); + + it('should set success to true upon success', () => { + // GIVEN + jest.spyOn(service, 'save').mockReturnValue(of(new HttpResponse({ body: true }))); + comp.passwordForm.patchValue({ + newPassword: 'myPassword', + confirmPassword: 'myPassword', + }); + + // WHEN + comp.changePassword(); + + // THEN + expect(comp.doNotMatch()).toBe(false); + expect(comp.error()).toBe(false); + expect(comp.success()).toBe(true); + }); + + it('should notify of error if change password fails', () => { + // GIVEN + jest.spyOn(service, 'save').mockReturnValue(throwError('ERROR')); + comp.passwordForm.patchValue({ + newPassword: 'myPassword', + confirmPassword: 'myPassword', + }); + + // WHEN + comp.changePassword(); + + // THEN + expect(comp.doNotMatch()).toBe(false); + expect(comp.success()).toBe(false); + expect(comp.error()).toBe(true); + }); +}); diff --git a/src/main/webapp/app/account/password/password.component.ts b/src/main/webapp/app/account/password/password.component.ts new file mode 100644 index 0000000..3338a16 --- /dev/null +++ b/src/main/webapp/app/account/password/password.component.ts @@ -0,0 +1,56 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Observable } from 'rxjs'; + +import SharedModule from 'app/shared/shared.module'; +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; +import { PasswordService } from './password.service'; +import PasswordStrengthBarComponent from './password-strength-bar/password-strength-bar.component'; + +@Component({ + standalone: true, + selector: 'jhi-password', + imports: [SharedModule, FormsModule, ReactiveFormsModule, PasswordStrengthBarComponent], + templateUrl: './password.component.html', +}) +export default class PasswordComponent implements OnInit { + doNotMatch = signal(false); + error = signal(false); + success = signal(false); + account$?: Observable; + passwordForm = new FormGroup({ + currentPassword: new FormControl('', { nonNullable: true, validators: Validators.required }), + newPassword: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)], + }), + confirmPassword: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)], + }), + }); + + private passwordService = inject(PasswordService); + private accountService = inject(AccountService); + + ngOnInit(): void { + this.account$ = this.accountService.identity(); + } + + changePassword(): void { + this.error.set(false); + this.success.set(false); + this.doNotMatch.set(false); + + const { newPassword, confirmPassword, currentPassword } = this.passwordForm.getRawValue(); + if (newPassword !== confirmPassword) { + this.doNotMatch.set(true); + } else { + this.passwordService.save(newPassword, currentPassword).subscribe({ + next: () => this.success.set(true), + error: () => this.error.set(true), + }); + } + } +} diff --git a/src/main/webapp/app/account/password/password.route.ts b/src/main/webapp/app/account/password/password.route.ts new file mode 100644 index 0000000..96faba9 --- /dev/null +++ b/src/main/webapp/app/account/password/password.route.ts @@ -0,0 +1,13 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import PasswordComponent from './password.component'; + +const passwordRoute: Route = { + path: 'password', + component: PasswordComponent, + title: 'global.menu.account.password', + canActivate: [UserRouteAccessService], +}; + +export default passwordRoute; diff --git a/src/main/webapp/app/account/password/password.service.spec.ts b/src/main/webapp/app/account/password/password.service.spec.ts new file mode 100644 index 0000000..b669e5a --- /dev/null +++ b/src/main/webapp/app/account/password/password.service.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { PasswordService } from './password.service'; + +describe('Password Service', () => { + let service: PasswordService; + let httpMock: HttpTestingController; + let applicationConfigService: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(PasswordService); + applicationConfigService = TestBed.inject(ApplicationConfigService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should call change-password endpoint with correct values', () => { + // GIVEN + const password1 = 'password1'; + const password2 = 'password2'; + + // WHEN + service.save(password2, password1).subscribe(); + + const testRequest = httpMock.expectOne({ + method: 'POST', + url: applicationConfigService.getEndpointFor('api/account/change-password'), + }); + + // THEN + expect(testRequest.request.body).toEqual({ currentPassword: password1, newPassword: password2 }); + }); + }); +}); diff --git a/src/main/webapp/app/account/password/password.service.ts b/src/main/webapp/app/account/password/password.service.ts new file mode 100644 index 0000000..3bfc0a2 --- /dev/null +++ b/src/main/webapp/app/account/password/password.service.ts @@ -0,0 +1,15 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; + +@Injectable({ providedIn: 'root' }) +export class PasswordService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + save(newPassword: string, currentPassword: string): Observable<{}> { + return this.http.post(this.applicationConfigService.getEndpointFor('api/account/change-password'), { currentPassword, newPassword }); + } +} diff --git a/src/main/webapp/app/account/register/register.component.html b/src/main/webapp/app/account/register/register.component.html new file mode 100644 index 0000000..6b6b672 --- /dev/null +++ b/src/main/webapp/app/account/register/register.component.html @@ -0,0 +1,220 @@ +
+
+
+

Registo

+ + @if (success()) { +
+ Registo efetuado com sucesso! Por favor verifique o seu email para confirmar a conta. +
+ } + + @if (error()) { +
+ Erro ao realizar o registo! Por favor tente novamente mais tarde. +
+ } + + @if (errorUserExists()) { +
+ Utilizador já registado! Por favor escolha outro. +
+ } + + @if (errorEmailExists()) { +
+ O email já está em uso! Por favor escolha outro. +
+ } + + @if (doNotMatch()) { +
+ A palavra-passe e a sua confirmação devem ser iguais! +
+ } +
+
+ +
+
+ @if (!success()) { +
+
+ + + + @if (registerForm.get('login')!.invalid && (registerForm.get('login')!.dirty || registerForm.get('login')!.touched)) { +
+ @if (registerForm.get('login')?.errors?.required) { + O utilizador é obrigatório. + } + + @if (registerForm.get('login')?.errors?.minlength) { + O utilizador deve ter pelo menos 1 caracter + } + + @if (registerForm.get('login')?.errors?.maxlength) { + O utilizador não pode ter mais de 50 caracteres + } + + @if (registerForm.get('login')?.errors?.pattern) { + Your username is invalid. + } +
+ } +
+ +
+ + + + @if (registerForm.get('email')!.invalid && (registerForm.get('email')!.dirty || registerForm.get('email')!.touched)) { +
+ @if (registerForm.get('email')?.errors?.required) { + O email é obrigatório. + } + + @if (registerForm.get('email')?.errors?.invalid) { + Email inválido. + } + + @if (registerForm.get('email')?.errors?.minlength) { + O email deve ter pelo menos 5 caracteres + } + + @if (registerForm.get('email')?.errors?.maxlength) { + O email não pode ter mais de 50 caracteres + } +
+ } +
+ +
+ + + + @if (registerForm.get('password')!.invalid && (registerForm.get('password')!.dirty || registerForm.get('password')!.touched)) { +
+ @if (registerForm.get('password')?.errors?.required) { + A palavra-passe é obrigatória. + } + + @if (registerForm.get('password')?.errors?.minlength) { + A palavra-passe deve ter pelo menos 4 caracteres + } + + @if (registerForm.get('password')?.errors?.maxlength) { + A palavra-passe não pode ter mais de 50 caracteres + } +
+ } + + +
+ +
+ + + + @if ( + registerForm.get('confirmPassword')!.invalid && + (registerForm.get('confirmPassword')!.dirty || registerForm.get('confirmPassword')!.touched) + ) { +
+ @if (registerForm.get('confirmPassword')?.errors?.required) { + A confirmação da palavra-passe é obrigatória. + } + + @if (registerForm.get('confirmPassword')?.errors?.minlength) { + A confirmação da palavra-passe deve ter pelo menos 4 caracteres + } + + @if (registerForm.get('confirmPassword')?.errors?.maxlength) { + A confirmação da palavra-passe não pode ter mais de 50 caracteres + } +
+ } +
+ + +
+ } + +
+ Para + entrar, utilize as seguintes contas:
- Administrador (utilizador="admin" e palavra-passe="admin")
- + Utilizador (utilizador="user" e palavra-passe="user").
+
+
+
+
diff --git a/src/main/webapp/app/account/register/register.component.spec.ts b/src/main/webapp/app/account/register/register.component.spec.ts new file mode 100644 index 0000000..e0855cc --- /dev/null +++ b/src/main/webapp/app/account/register/register.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed, waitForAsync, inject, tick, fakeAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { of, throwError } from 'rxjs'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; + +import { EMAIL_ALREADY_USED_TYPE, LOGIN_ALREADY_USED_TYPE } from 'app/config/error.constants'; + +import { RegisterService } from './register.service'; +import RegisterComponent from './register.component'; + +describe('RegisterComponent', () => { + let fixture: ComponentFixture; + let comp: RegisterComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), HttpClientTestingModule, RegisterComponent], + providers: [FormBuilder], + }) + .overrideTemplate(RegisterComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegisterComponent); + comp = fixture.componentInstance; + }); + + it('should ensure the two passwords entered match', () => { + comp.registerForm.patchValue({ + password: 'password', + confirmPassword: 'non-matching', + }); + + comp.register(); + + expect(comp.doNotMatch()).toBe(true); + }); + + it('should update success to true after creating an account', inject( + [RegisterService, TranslateService], + fakeAsync((service: RegisterService, mockTranslateService: TranslateService) => { + jest.spyOn(service, 'save').mockReturnValue(of({})); + mockTranslateService.currentLang = 'pt-pt'; + comp.registerForm.patchValue({ + password: 'password', + confirmPassword: 'password', + }); + + comp.register(); + tick(); + + expect(service.save).toHaveBeenCalledWith({ + email: '', + password: 'password', + login: '', + langKey: 'pt-pt', + }); + expect(comp.success()).toBe(true); + expect(comp.errorUserExists()).toBe(false); + expect(comp.errorEmailExists()).toBe(false); + expect(comp.error()).toBe(false); + }), + )); + + it('should notify of user existence upon 400/login already in use', inject( + [RegisterService], + fakeAsync((service: RegisterService) => { + const err = { status: 400, error: { type: LOGIN_ALREADY_USED_TYPE } }; + jest.spyOn(service, 'save').mockReturnValue(throwError(() => err)); + comp.registerForm.patchValue({ + password: 'password', + confirmPassword: 'password', + }); + + comp.register(); + tick(); + + expect(comp.errorUserExists()).toBe(true); + expect(comp.errorEmailExists()).toBe(false); + expect(comp.error()).toBe(false); + }), + )); + + it('should notify of email existence upon 400/email address already in use', inject( + [RegisterService], + fakeAsync((service: RegisterService) => { + const err = { status: 400, error: { type: EMAIL_ALREADY_USED_TYPE } }; + jest.spyOn(service, 'save').mockReturnValue(throwError(() => err)); + comp.registerForm.patchValue({ + password: 'password', + confirmPassword: 'password', + }); + + comp.register(); + tick(); + + expect(comp.errorEmailExists()).toBe(true); + expect(comp.errorUserExists()).toBe(false); + expect(comp.error()).toBe(false); + }), + )); + + it('should notify of generic error', inject( + [RegisterService], + fakeAsync((service: RegisterService) => { + const err = { status: 503 }; + jest.spyOn(service, 'save').mockReturnValue(throwError(() => err)); + comp.registerForm.patchValue({ + password: 'password', + confirmPassword: 'password', + }); + + comp.register(); + tick(); + + expect(comp.errorUserExists()).toBe(false); + expect(comp.errorEmailExists()).toBe(false); + expect(comp.error()).toBe(true); + }), + )); +}); diff --git a/src/main/webapp/app/account/register/register.component.ts b/src/main/webapp/app/account/register/register.component.ts new file mode 100644 index 0000000..52a5674 --- /dev/null +++ b/src/main/webapp/app/account/register/register.component.ts @@ -0,0 +1,84 @@ +import { Component, AfterViewInit, ElementRef, inject, signal, viewChild } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { RouterModule } from '@angular/router'; +import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; + +import { EMAIL_ALREADY_USED_TYPE, LOGIN_ALREADY_USED_TYPE } from 'app/config/error.constants'; +import SharedModule from 'app/shared/shared.module'; +import PasswordStrengthBarComponent from '../password/password-strength-bar/password-strength-bar.component'; +import { RegisterService } from './register.service'; + +@Component({ + standalone: true, + selector: 'jhi-register', + imports: [SharedModule, RouterModule, FormsModule, ReactiveFormsModule, PasswordStrengthBarComponent], + templateUrl: './register.component.html', +}) +export default class RegisterComponent implements AfterViewInit { + login = viewChild.required('login'); + + doNotMatch = signal(false); + error = signal(false); + errorEmailExists = signal(false); + errorUserExists = signal(false); + success = signal(false); + + registerForm = new FormGroup({ + login: new FormControl('', { + nonNullable: true, + validators: [ + Validators.required, + Validators.minLength(1), + Validators.maxLength(50), + Validators.pattern('^[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$|^[_.@A-Za-z0-9-]+$'), + ], + }), + email: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email], + }), + password: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)], + }), + confirmPassword: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)], + }), + }); + + private translateService = inject(TranslateService); + private registerService = inject(RegisterService); + + ngAfterViewInit(): void { + this.login().nativeElement.focus(); + } + + register(): void { + this.doNotMatch.set(false); + this.error.set(false); + this.errorEmailExists.set(false); + this.errorUserExists.set(false); + + const { password, confirmPassword } = this.registerForm.getRawValue(); + if (password !== confirmPassword) { + this.doNotMatch.set(true); + } else { + const { login, email } = this.registerForm.getRawValue(); + this.registerService + .save({ login, email, password, langKey: this.translateService.currentLang }) + .subscribe({ next: () => this.success.set(true), error: response => this.processError(response) }); + } + } + + private processError(response: HttpErrorResponse): void { + if (response.status === 400 && response.error.type === LOGIN_ALREADY_USED_TYPE) { + this.errorUserExists.set(true); + } else if (response.status === 400 && response.error.type === EMAIL_ALREADY_USED_TYPE) { + this.errorEmailExists.set(true); + } else { + this.error.set(true); + } + } +} diff --git a/src/main/webapp/app/account/register/register.model.ts b/src/main/webapp/app/account/register/register.model.ts new file mode 100644 index 0000000..02e04a1 --- /dev/null +++ b/src/main/webapp/app/account/register/register.model.ts @@ -0,0 +1,8 @@ +export class Registration { + constructor( + public login: string, + public email: string, + public password: string, + public langKey: string, + ) {} +} diff --git a/src/main/webapp/app/account/register/register.route.ts b/src/main/webapp/app/account/register/register.route.ts new file mode 100644 index 0000000..81323c3 --- /dev/null +++ b/src/main/webapp/app/account/register/register.route.ts @@ -0,0 +1,11 @@ +import { Route } from '@angular/router'; + +import RegisterComponent from './register.component'; + +const registerRoute: Route = { + path: 'register', + component: RegisterComponent, + title: 'register.title', +}; + +export default registerRoute; diff --git a/src/main/webapp/app/account/register/register.service.spec.ts b/src/main/webapp/app/account/register/register.service.spec.ts new file mode 100644 index 0000000..fb9d8a7 --- /dev/null +++ b/src/main/webapp/app/account/register/register.service.spec.ts @@ -0,0 +1,48 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { RegisterService } from './register.service'; +import { Registration } from './register.model'; + +describe('RegisterService Service', () => { + let service: RegisterService; + let httpMock: HttpTestingController; + let applicationConfigService: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(RegisterService); + applicationConfigService = TestBed.inject(ApplicationConfigService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should call register endpoint with correct values', () => { + // GIVEN + const login = 'abc'; + const email = 'test@test.com'; + const password = 'pass'; + const langKey = 'FR'; + const registration = new Registration(login, email, password, langKey); + + // WHEN + service.save(registration).subscribe(); + + const testRequest = httpMock.expectOne({ + method: 'POST', + url: applicationConfigService.getEndpointFor('api/register'), + }); + + // THEN + expect(testRequest.request.body).toEqual({ email, langKey, login, password }); + }); + }); +}); diff --git a/src/main/webapp/app/account/register/register.service.ts b/src/main/webapp/app/account/register/register.service.ts new file mode 100644 index 0000000..8b6a6ff --- /dev/null +++ b/src/main/webapp/app/account/register/register.service.ts @@ -0,0 +1,16 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { Registration } from './register.model'; + +@Injectable({ providedIn: 'root' }) +export class RegisterService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + save(registration: Registration): Observable<{}> { + return this.http.post(this.applicationConfigService.getEndpointFor('api/register'), registration); + } +} diff --git a/src/main/webapp/app/account/sessions/session.model.ts b/src/main/webapp/app/account/sessions/session.model.ts new file mode 100644 index 0000000..d974afb --- /dev/null +++ b/src/main/webapp/app/account/sessions/session.model.ts @@ -0,0 +1,8 @@ +export class Session { + constructor( + public series: string, + public tokenDate: Date, + public ipAddress: string, + public userAgent: string, + ) {} +} diff --git a/src/main/webapp/app/account/sessions/sessions.component.html b/src/main/webapp/app/account/sessions/sessions.component.html new file mode 100644 index 0000000..76b9973 --- /dev/null +++ b/src/main/webapp/app/account/sessions/sessions.component.html @@ -0,0 +1,45 @@ +
+ @if (account) { +

+ Sessões ativas para [{{ account.login }}] +

+ } + + @if (success) { +
Sessão terminada com sucesso!
+ } + + @if (error) { +
+ Ocorreu um erro! A sessão não pode ser terminada. +
+ } + +
+ + + + + + + + + + + @for (session of sessions; track $index) { + + + + + + + } + +
IPAgenteData
{{ session.ipAddress }}{{ session.userAgent }}{{ session.tokenDate | date: 'longDate' }} + +
+
+
diff --git a/src/main/webapp/app/account/sessions/sessions.component.spec.ts b/src/main/webapp/app/account/sessions/sessions.component.spec.ts new file mode 100644 index 0000000..88b7959 --- /dev/null +++ b/src/main/webapp/app/account/sessions/sessions.component.spec.ts @@ -0,0 +1,103 @@ +jest.mock('app/core/auth/account.service'); + +import { ComponentFixture, TestBed, inject, tick, fakeAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of, throwError } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; + +import { Session } from './session.model'; +import SessionsComponent from './sessions.component'; +import { SessionsService } from './sessions.service'; + +describe('SessionsComponent', () => { + let fixture: ComponentFixture; + let comp: SessionsComponent; + const sessions: Session[] = [new Session('xxxxxx==', new Date(2015, 10, 15), '0:0:0:0:0:0:0:1', 'Mozilla/5.0')]; + const account: Account = { + firstName: 'John', + lastName: 'Doe', + activated: true, + email: 'john.doe@mail.com', + langKey: 'pt-pt', + login: 'john', + authorities: [], + imageUrl: '', + }; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, SessionsComponent], + providers: [AccountService], + }) + .overrideTemplate(SessionsComponent, '') + .createComponent(SessionsComponent); + comp = fixture.componentInstance; + }); + + it('should define its initial state', inject( + [AccountService, SessionsService], + fakeAsync((mockAccountService: AccountService, service: SessionsService) => { + mockAccountService.identity = jest.fn(() => of(account)); + jest.spyOn(service, 'findAll').mockReturnValue(of(sessions)); + + comp.ngOnInit(); + tick(); + + expect(mockAccountService.identity).toHaveBeenCalled(); + expect(service.findAll).toHaveBeenCalled(); + expect(comp.success).toBe(false); + expect(comp.error).toBe(false); + expect(comp.account).toEqual(account); + expect(comp.sessions).toEqual(sessions); + }), + )); + + it('should call delete on Sessions to invalidate a session', inject( + [AccountService, SessionsService], + fakeAsync((mockAccountService: AccountService, service: SessionsService) => { + mockAccountService.identity = jest.fn(() => of(account)); + jest.spyOn(service, 'findAll').mockReturnValue(of(sessions)); + jest.spyOn(service, 'delete').mockReturnValue(of({})); + + comp.ngOnInit(); + comp.invalidate('xyz'); + tick(); + + expect(service.delete).toHaveBeenCalledWith('xyz'); + }), + )); + + it('should call delete on Sessions and notify of error', inject( + [AccountService, SessionsService], + fakeAsync((mockAccountService: AccountService, service: SessionsService) => { + mockAccountService.identity = jest.fn(() => of(account)); + jest.spyOn(service, 'findAll').mockReturnValue(of(sessions)); + jest.spyOn(service, 'delete').mockReturnValue(throwError(() => {})); + + comp.ngOnInit(); + comp.invalidate('xyz'); + tick(); + + expect(comp.success).toBe(false); + expect(comp.error).toBe(true); + }), + )); + + it('should call notify of success upon session invalidation', inject( + [AccountService, SessionsService], + fakeAsync((mockAccountService: AccountService, service: SessionsService) => { + mockAccountService.identity = jest.fn(() => of(account)); + jest.spyOn(service, 'findAll').mockReturnValue(of(sessions)); + jest.spyOn(service, 'delete').mockReturnValue(of({})); + + comp.ngOnInit(); + comp.invalidate('xyz'); + tick(); + + expect(comp.error).toBe(false); + expect(comp.success).toBe(true); + }), + )); +}); diff --git a/src/main/webapp/app/account/sessions/sessions.component.ts b/src/main/webapp/app/account/sessions/sessions.component.ts new file mode 100644 index 0000000..1e84ad0 --- /dev/null +++ b/src/main/webapp/app/account/sessions/sessions.component.ts @@ -0,0 +1,42 @@ +import { Component, inject, OnInit } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; +import { Session } from './session.model'; +import { SessionsService } from './sessions.service'; + +@Component({ + standalone: true, + selector: 'jhi-sessions', + imports: [SharedModule], + templateUrl: './sessions.component.html', +}) +export default class SessionsComponent implements OnInit { + account: Account | null = null; + error = false; + success = false; + sessions: Session[] = []; + + private sessionsService = inject(SessionsService); + private accountService = inject(AccountService); + + ngOnInit(): void { + this.sessionsService.findAll().subscribe(sessions => (this.sessions = sessions)); + + this.accountService.identity().subscribe(account => (this.account = account)); + } + + invalidate(series: string): void { + this.error = false; + this.success = false; + + this.sessionsService.delete(encodeURIComponent(series)).subscribe( + () => { + this.success = true; + this.sessionsService.findAll().subscribe(sessions => (this.sessions = sessions)); + }, + () => (this.error = true), + ); + } +} diff --git a/src/main/webapp/app/account/sessions/sessions.route.ts b/src/main/webapp/app/account/sessions/sessions.route.ts new file mode 100644 index 0000000..be96c0b --- /dev/null +++ b/src/main/webapp/app/account/sessions/sessions.route.ts @@ -0,0 +1,13 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import SessionsComponent from './sessions.component'; + +const sessionsRoute: Route = { + path: 'sessions', + component: SessionsComponent, + title: 'global.menu.account.sessions', + canActivate: [UserRouteAccessService], +}; + +export default sessionsRoute; diff --git a/src/main/webapp/app/account/sessions/sessions.service.ts b/src/main/webapp/app/account/sessions/sessions.service.ts new file mode 100644 index 0000000..d138599 --- /dev/null +++ b/src/main/webapp/app/account/sessions/sessions.service.ts @@ -0,0 +1,22 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { Session } from './session.model'; + +@Injectable({ providedIn: 'root' }) +export class SessionsService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/account/sessions'); + + findAll(): Observable { + return this.http.get(this.resourceUrl); + } + + delete(series: string): Observable<{}> { + return this.http.delete(`${this.resourceUrl}/${series}`); + } +} diff --git a/src/main/webapp/app/account/settings/settings.component.html b/src/main/webapp/app/account/settings/settings.component.html new file mode 100644 index 0000000..465d990 --- /dev/null +++ b/src/main/webapp/app/account/settings/settings.component.html @@ -0,0 +1,154 @@ +
+
+
+ @if (settingsForm.value.login) { +

+ Configurações para o utilizador [{{ settingsForm.value.login }}] +

+ } + + @if (success()) { +
+ Configurações guardadas com sucesso! +
+ } + + + + @if (settingsForm.value.login) { +
+
+ + + + @if ( + settingsForm.get('firstName')!.invalid && (settingsForm.get('firstName')!.dirty || settingsForm.get('firstName')!.touched) + ) { +
+ @if (settingsForm.get('firstName')?.errors?.required) { + O nome é obrigatório. + } + + @if (settingsForm.get('firstName')?.errors?.minlength) { + O nome deve ter pelo menos 1 caracter + } + + @if (settingsForm.get('firstName')?.errors?.maxlength) { + O nome não pode ter mais de 50 caracteres + } +
+ } +
+ +
+ + + + @if (settingsForm.get('lastName')!.invalid && (settingsForm.get('lastName')!.dirty || settingsForm.get('lastName')!.touched)) { +
+ @if (settingsForm.get('lastName')?.errors?.required) { + O apelido é obrigatório. + } + + @if (settingsForm.get('lastName')?.errors?.minlength) { + O apelido deve ter pelo menos 1 caracter + } + + @if (settingsForm.get('lastName')?.errors?.maxlength) { + O apelido não pode ter mais de 50 caracteres + } +
+ } +
+ +
+ + + + @if (settingsForm.get('email')!.invalid && (settingsForm.get('email')!.dirty || settingsForm.get('email')!.touched)) { +
+ @if (settingsForm.get('email')?.errors?.required) { + O email é obrigatório. + } + + @if (settingsForm.get('email')?.errors?.email) { + Email inválido. + } + + @if (settingsForm.get('email')?.errors?.minlength) { + O email deve ter pelo menos 5 caracteres + } + + @if (settingsForm.get('email')?.errors?.maxlength) { + O email não pode ter mais de 50 caracteres + } +
+ } +
+ + @if (languages && languages.length > 0) { +
+ + +
+ } + + +
+ } +
+
+
diff --git a/src/main/webapp/app/account/settings/settings.component.spec.ts b/src/main/webapp/app/account/settings/settings.component.spec.ts new file mode 100644 index 0000000..b16a4d8 --- /dev/null +++ b/src/main/webapp/app/account/settings/settings.component.spec.ts @@ -0,0 +1,90 @@ +jest.mock('app/core/auth/account.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { throwError, of } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; + +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; + +import SettingsComponent from './settings.component'; + +describe('SettingsComponent', () => { + let comp: SettingsComponent; + let fixture: ComponentFixture; + let mockAccountService: AccountService; + const account: Account = { + firstName: 'John', + lastName: 'Doe', + activated: true, + email: 'john.doe@mail.com', + langKey: 'pt-pt', + login: 'john', + authorities: [], + imageUrl: '', + }; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), HttpClientTestingModule, SettingsComponent], + providers: [FormBuilder, AccountService], + }) + .overrideTemplate(SettingsComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsComponent); + comp = fixture.componentInstance; + mockAccountService = TestBed.inject(AccountService); + mockAccountService.identity = jest.fn(() => of(account)); + mockAccountService.getAuthenticationState = jest.fn(() => of(account)); + }); + + it('should send the current identity upon save', () => { + // GIVEN + mockAccountService.save = jest.fn(() => of({})); + const settingsFormValues = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@mail.com', + langKey: 'pt-pt', + }; + + // WHEN + comp.ngOnInit(); + comp.save(); + + // THEN + expect(mockAccountService.identity).toHaveBeenCalled(); + expect(mockAccountService.save).toHaveBeenCalledWith(account); + expect(mockAccountService.authenticate).toHaveBeenCalledWith(account); + expect(comp.settingsForm.value).toMatchObject(expect.objectContaining(settingsFormValues)); + }); + + it('should notify of success upon successful save', () => { + // GIVEN + mockAccountService.save = jest.fn(() => of({})); + + // WHEN + comp.ngOnInit(); + comp.save(); + + // THEN + expect(comp.success()).toBe(true); + }); + + it('should notify of error upon failed save', () => { + // GIVEN + mockAccountService.save = jest.fn(() => throwError('ERROR')); + + // WHEN + comp.ngOnInit(); + comp.save(); + + // THEN + expect(comp.success()).toBe(false); + }); +}); diff --git a/src/main/webapp/app/account/settings/settings.component.ts b/src/main/webapp/app/account/settings/settings.component.ts new file mode 100644 index 0000000..7e86771 --- /dev/null +++ b/src/main/webapp/app/account/settings/settings.component.ts @@ -0,0 +1,74 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; + +import SharedModule from 'app/shared/shared.module'; +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; +import { LANGUAGES } from 'app/config/language.constants'; + +const initialAccount: Account = {} as Account; + +@Component({ + standalone: true, + selector: 'jhi-settings', + imports: [SharedModule, FormsModule, ReactiveFormsModule], + templateUrl: './settings.component.html', +}) +export default class SettingsComponent implements OnInit { + success = signal(false); + languages = LANGUAGES; + + settingsForm = new FormGroup({ + firstName: new FormControl(initialAccount.firstName, { + nonNullable: true, + validators: [Validators.required, Validators.minLength(1), Validators.maxLength(50)], + }), + lastName: new FormControl(initialAccount.lastName, { + nonNullable: true, + validators: [Validators.required, Validators.minLength(1), Validators.maxLength(50)], + }), + email: new FormControl(initialAccount.email, { + nonNullable: true, + validators: [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email], + }), + langKey: new FormControl(initialAccount.langKey, { nonNullable: true }), + + activated: new FormControl(initialAccount.activated, { nonNullable: true }), + authorities: new FormControl(initialAccount.authorities, { nonNullable: true }), + imageUrl: new FormControl(initialAccount.imageUrl, { nonNullable: true }), + login: new FormControl(initialAccount.login, { nonNullable: true }), + }); + + private accountService = inject(AccountService); + private translateService = inject(TranslateService); + + ngOnInit(): void { + this.accountService.identity().subscribe(account => { + if (account) { + this.settingsForm.patchValue(account); + } + }); + } + + save(): void { + this.success.set(false); + + // const account = this.settingsForm.getRawValue(); + const account: Account = { + ...this.settingsForm.getRawValue(), + securityGroup: null, + parentOrganization: null, + }; + + this.accountService.save(account).subscribe(() => { + this.success.set(true); + + this.accountService.authenticate(account); + + if (account.langKey !== this.translateService.currentLang) { + this.translateService.use(account.langKey); + } + }); + } +} diff --git a/src/main/webapp/app/account/settings/settings.route.ts b/src/main/webapp/app/account/settings/settings.route.ts new file mode 100644 index 0000000..3c93066 --- /dev/null +++ b/src/main/webapp/app/account/settings/settings.route.ts @@ -0,0 +1,13 @@ +import { Route } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import SettingsComponent from './settings.component'; + +const settingsRoute: Route = { + path: 'settings', + component: SettingsComponent, + title: 'global.menu.account.settings', + canActivate: [UserRouteAccessService], +}; + +export default settingsRoute; diff --git a/src/main/webapp/app/admin/admin.routes.ts b/src/main/webapp/app/admin/admin.routes.ts new file mode 100644 index 0000000..cfb8bc2 --- /dev/null +++ b/src/main/webapp/app/admin/admin.routes.ts @@ -0,0 +1,38 @@ +import { Routes } from '@angular/router'; +/* jhipster-needle-add-admin-module-import - JHipster will add admin modules imports here */ + +const routes: Routes = [ + { + path: 'user-management', + loadChildren: () => import('./user-management/user-management.route'), + title: 'userManagement.home.title', + }, + { + path: 'docs', + loadComponent: () => import('./docs/docs.component'), + title: 'global.menu.admin.apidocs', + }, + { + path: 'configuration', + loadComponent: () => import('./configuration/configuration.component'), + title: 'configuration.title', + }, + { + path: 'health', + loadComponent: () => import('./health/health.component'), + title: 'health.title', + }, + { + path: 'logs', + loadComponent: () => import('./logs/logs.component'), + title: 'logs.title', + }, + { + path: 'metrics', + loadComponent: () => import('./metrics/metrics.component'), + title: 'metrics.title', + }, + /* jhipster-needle-add-admin-route - JHipster will add admin routes here */ +]; + +export default routes; diff --git a/src/main/webapp/app/admin/configuration/configuration.component.html b/src/main/webapp/app/admin/configuration/configuration.component.html new file mode 100644 index 0000000..4c3e577 --- /dev/null +++ b/src/main/webapp/app/admin/configuration/configuration.component.html @@ -0,0 +1,70 @@ +@if (allBeans()) { +
+

Configuração

+ + Filtrar (por prefixo) + + +

Spring configuration

+ + + + + + + + + + @for (bean of beans(); track $index) { + + + + + } + +
+ Prefixo + Propriedades
+ {{ bean.prefix }} + + @for (property of bean.properties | keyvalue; track property.key) { +
+
{{ property.key }}
+
+ {{ property.value | json }} +
+
+ } +
+ + @for (propertySource of propertySources(); track i; let i = $index) { +
+

+ {{ propertySource.name }} +

+ + + + + + + + + + @for (property of propertySource.properties | keyvalue; track property.key) { + + + + + } + +
PropertyValue
{{ property.key }} + {{ property.value.value }} +
+
+ } +
+} diff --git a/src/main/webapp/app/admin/configuration/configuration.component.spec.ts b/src/main/webapp/app/admin/configuration/configuration.component.spec.ts new file mode 100644 index 0000000..be39e5f --- /dev/null +++ b/src/main/webapp/app/admin/configuration/configuration.component.spec.ts @@ -0,0 +1,66 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; + +import ConfigurationComponent from './configuration.component'; +import { ConfigurationService } from './configuration.service'; +import { Bean, PropertySource } from './configuration.model'; + +describe('ConfigurationComponent', () => { + let comp: ConfigurationComponent; + let fixture: ComponentFixture; + let service: ConfigurationService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, ConfigurationComponent], + providers: [ConfigurationService], + }) + .overrideTemplate(ConfigurationComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfigurationComponent); + comp = fixture.componentInstance; + service = TestBed.inject(ConfigurationService); + }); + + describe('OnInit', () => { + it('Should call load all on init', () => { + // GIVEN + const beans: Bean[] = [ + { + prefix: 'jhipster', + properties: { + clientApp: { + name: 'jhipsterApp', + }, + }, + }, + ]; + const propertySources: PropertySource[] = [ + { + name: 'server.ports', + properties: { + 'local.server.port': { + value: '8080', + }, + }, + }, + ]; + jest.spyOn(service, 'getBeans').mockReturnValue(of(beans)); + jest.spyOn(service, 'getPropertySources').mockReturnValue(of(propertySources)); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.getBeans).toHaveBeenCalled(); + expect(service.getPropertySources).toHaveBeenCalled(); + expect(comp.allBeans()).toEqual(beans); + expect(comp.beans()).toEqual(beans); + expect(comp.propertySources()).toEqual(propertySources); + }); + }); +}); diff --git a/src/main/webapp/app/admin/configuration/configuration.component.ts b/src/main/webapp/app/admin/configuration/configuration.component.ts new file mode 100644 index 0000000..ba5949b --- /dev/null +++ b/src/main/webapp/app/admin/configuration/configuration.component.ts @@ -0,0 +1,44 @@ +import { Component, computed, inject, OnInit, signal } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule } from '@angular/forms'; +import { SortDirective, SortByDirective, sortStateSignal, SortService } from 'app/shared/sort'; +import { ConfigurationService } from './configuration.service'; +import { Bean, PropertySource } from './configuration.model'; + +@Component({ + standalone: true, + selector: 'jhi-configuration', + templateUrl: './configuration.component.html', + imports: [SharedModule, FormsModule, SortDirective, SortByDirective], +}) +export default class ConfigurationComponent implements OnInit { + allBeans = signal(undefined); + beansFilter = signal(''); + propertySources = signal([]); + sortState = sortStateSignal({ predicate: 'prefix', order: 'asc' }); + beans = computed(() => { + let data = this.allBeans() ?? []; + const beansFilter = this.beansFilter(); + if (beansFilter) { + data = data.filter(bean => bean.prefix.toLowerCase().includes(beansFilter.toLowerCase())); + } + + const { order, predicate } = this.sortState(); + if (predicate && order) { + data = data.sort(this.sortService.startSort({ predicate, order })); + } + return data; + }); + + private sortService = inject(SortService); + private configurationService = inject(ConfigurationService); + + ngOnInit(): void { + this.configurationService.getBeans().subscribe(beans => { + this.allBeans.set(beans); + }); + + this.configurationService.getPropertySources().subscribe(propertySources => this.propertySources.set(propertySources)); + } +} diff --git a/src/main/webapp/app/admin/configuration/configuration.model.ts b/src/main/webapp/app/admin/configuration/configuration.model.ts new file mode 100644 index 0000000..6a671e0 --- /dev/null +++ b/src/main/webapp/app/admin/configuration/configuration.model.ts @@ -0,0 +1,40 @@ +export interface ConfigProps { + contexts: Contexts; +} + +export interface Contexts { + [key: string]: Context; +} + +export interface Context { + beans: Beans; + parentId?: any; +} + +export interface Beans { + [key: string]: Bean; +} + +export interface Bean { + prefix: string; + properties: any; +} + +export interface Env { + activeProfiles?: string[]; + propertySources: PropertySource[]; +} + +export interface PropertySource { + name: string; + properties: Properties; +} + +export interface Properties { + [key: string]: Property; +} + +export interface Property { + value: string; + origin?: string; +} diff --git a/src/main/webapp/app/admin/configuration/configuration.service.spec.ts b/src/main/webapp/app/admin/configuration/configuration.service.spec.ts new file mode 100644 index 0000000..6e6ff7f --- /dev/null +++ b/src/main/webapp/app/admin/configuration/configuration.service.spec.ts @@ -0,0 +1,71 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ConfigurationService } from './configuration.service'; +import { Bean, ConfigProps, Env, PropertySource } from './configuration.model'; + +describe('Logs Service', () => { + let service: ConfigurationService; + let httpMock: HttpTestingController; + let expectedResult: Bean[] | PropertySource[] | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + expectedResult = null; + service = TestBed.inject(ConfigurationService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should get the config', () => { + const bean: Bean = { + prefix: 'jhipster', + properties: { + clientApp: { + name: 'jhipsterApp', + }, + }, + }; + const configProps: ConfigProps = { + contexts: { + jhipster: { + beans: { + 'tech.jhipster.config.JHipsterProperties': bean, + }, + }, + }, + }; + service.getBeans().subscribe(received => (expectedResult = received)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(configProps); + expect(expectedResult).toEqual([bean]); + }); + + it('should get the env', () => { + const propertySources: PropertySource[] = [ + { + name: 'server.ports', + properties: { + 'local.server.port': { + value: '8080', + }, + }, + }, + ]; + const env: Env = { propertySources }; + service.getPropertySources().subscribe(received => (expectedResult = received)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(env); + expect(expectedResult).toEqual(propertySources); + }); + }); +}); diff --git a/src/main/webapp/app/admin/configuration/configuration.service.ts b/src/main/webapp/app/admin/configuration/configuration.service.ts new file mode 100644 index 0000000..eecb1c6 --- /dev/null +++ b/src/main/webapp/app/admin/configuration/configuration.service.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { Bean, Beans, ConfigProps, Env, PropertySource } from './configuration.model'; + +@Injectable({ providedIn: 'root' }) +export class ConfigurationService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + getBeans(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('management/configprops')).pipe( + map(configProps => + Object.values( + Object.values(configProps.contexts) + .map(context => context.beans) + .reduce((allBeans: Beans, contextBeans: Beans) => ({ ...allBeans, ...contextBeans }), {}), + ), + ), + ); + } + + getPropertySources(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('management/env')).pipe(map(env => env.propertySources)); + } +} diff --git a/src/main/webapp/app/admin/docs/docs.component.html b/src/main/webapp/app/admin/docs/docs.component.html new file mode 100644 index 0000000..2402552 --- /dev/null +++ b/src/main/webapp/app/admin/docs/docs.component.html @@ -0,0 +1,10 @@ + diff --git a/src/main/webapp/app/admin/docs/docs.component.scss b/src/main/webapp/app/admin/docs/docs.component.scss new file mode 100644 index 0000000..bb9a6cc --- /dev/null +++ b/src/main/webapp/app/admin/docs/docs.component.scss @@ -0,0 +1,6 @@ +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; + +iframe { + background: white; +} diff --git a/src/main/webapp/app/admin/docs/docs.component.ts b/src/main/webapp/app/admin/docs/docs.component.ts new file mode 100644 index 0000000..150ea5c --- /dev/null +++ b/src/main/webapp/app/admin/docs/docs.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'jhi-docs', + templateUrl: './docs.component.html', + styleUrl: './docs.component.scss', +}) +export default class DocsComponent {} diff --git a/src/main/webapp/app/admin/health/health.component.html b/src/main/webapp/app/admin/health/health.component.html new file mode 100644 index 0000000..104bf0b --- /dev/null +++ b/src/main/webapp/app/admin/health/health.component.html @@ -0,0 +1,61 @@ +
+

+ Estado do Sistema + + +

+ +
+ + + + + + + + + @if (health) { + + @for (componentHealth of health.components | keyvalue; track componentHealth.key) { + + + + + + } + + } +
Nome do ServiçoEstadoDetails
+ {{ + { + diskSpace: 'Espaço em disco', + mail: 'Email', + livenessState: 'Liveness state', + readinessState: 'Readiness state', + ping: 'Application', + db: 'Base de dados' + }[componentHealth.key] || componentHealth.key + }} + + + {{ + { UNKNOWN: 'UNKNOWN', UP: 'UP', OUT_OF_SERVICE: 'OUT_OF_SERVICE', DOWN: 'DOWN' }[ + componentHealth.value?.status ?? 'UNKNOWN' + ] + }} + + + @if (componentHealth.value!.details) { + + + + } +
+
+
diff --git a/src/main/webapp/app/admin/health/health.component.spec.ts b/src/main/webapp/app/admin/health/health.component.spec.ts new file mode 100644 index 0000000..97ce8ab --- /dev/null +++ b/src/main/webapp/app/admin/health/health.component.spec.ts @@ -0,0 +1,65 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of, throwError } from 'rxjs'; + +import HealthComponent from './health.component'; +import { HealthService } from './health.service'; +import { Health } from './health.model'; + +describe('HealthComponent', () => { + let comp: HealthComponent; + let fixture: ComponentFixture; + let service: HealthService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, HealthComponent], + }) + .overrideTemplate(HealthComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthComponent); + comp = fixture.componentInstance; + service = TestBed.inject(HealthService); + }); + + describe('getBadgeClass', () => { + it('should get badge class', () => { + const upBadgeClass = comp.getBadgeClass('UP'); + const downBadgeClass = comp.getBadgeClass('DOWN'); + expect(upBadgeClass).toEqual('bg-success'); + expect(downBadgeClass).toEqual('bg-danger'); + }); + }); + + describe('refresh', () => { + it('should call refresh on init', () => { + // GIVEN + const health: Health = { status: 'UP', components: { mail: { status: 'UP', details: { mailDetail: 'mail' } } } }; + jest.spyOn(service, 'checkHealth').mockReturnValue(of(health)); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.checkHealth).toHaveBeenCalled(); + expect(comp.health).toEqual(health); + }); + + it('should handle a 503 on refreshing health data', () => { + // GIVEN + const health: Health = { status: 'DOWN', components: { mail: { status: 'DOWN' } } }; + jest.spyOn(service, 'checkHealth').mockReturnValue(throwError(new HttpErrorResponse({ status: 503, error: health }))); + + // WHEN + comp.refresh(); + + // THEN + expect(service.checkHealth).toHaveBeenCalled(); + expect(comp.health).toEqual(health); + }); + }); +}); diff --git a/src/main/webapp/app/admin/health/health.component.ts b/src/main/webapp/app/admin/health/health.component.ts new file mode 100644 index 0000000..392b750 --- /dev/null +++ b/src/main/webapp/app/admin/health/health.component.ts @@ -0,0 +1,48 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { HealthService } from './health.service'; +import { Health, HealthDetails, HealthStatus } from './health.model'; +import HealthModalComponent from './modal/health-modal.component'; + +@Component({ + standalone: true, + selector: 'jhi-health', + templateUrl: './health.component.html', + imports: [SharedModule, HealthModalComponent], +}) +export default class HealthComponent implements OnInit { + health?: Health; + + private modalService = inject(NgbModal); + private healthService = inject(HealthService); + + ngOnInit(): void { + this.refresh(); + } + + getBadgeClass(statusState: HealthStatus): string { + if (statusState === 'UP') { + return 'bg-success'; + } + return 'bg-danger'; + } + + refresh(): void { + this.healthService.checkHealth().subscribe({ + next: health => (this.health = health), + error: (error: HttpErrorResponse) => { + if (error.status === 503) { + this.health = error.error; + } + }, + }); + } + + showHealth(health: { key: string; value: HealthDetails }): void { + const modalRef = this.modalService.open(HealthModalComponent); + modalRef.componentInstance.health = health; + } +} diff --git a/src/main/webapp/app/admin/health/health.model.ts b/src/main/webapp/app/admin/health/health.model.ts new file mode 100644 index 0000000..0811289 --- /dev/null +++ b/src/main/webapp/app/admin/health/health.model.ts @@ -0,0 +1,15 @@ +export type HealthStatus = 'UP' | 'DOWN' | 'UNKNOWN' | 'OUT_OF_SERVICE'; + +export type HealthKey = 'diskSpace' | 'mail' | 'ping' | 'livenessState' | 'readinessState' | 'db'; + +export interface Health { + status: HealthStatus; + components: { + [key in HealthKey]?: HealthDetails; + }; +} + +export interface HealthDetails { + status: HealthStatus; + details?: { [key: string]: unknown }; +} diff --git a/src/main/webapp/app/admin/health/health.service.spec.ts b/src/main/webapp/app/admin/health/health.service.spec.ts new file mode 100644 index 0000000..1e1ff19 --- /dev/null +++ b/src/main/webapp/app/admin/health/health.service.spec.ts @@ -0,0 +1,48 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { HealthService } from './health.service'; + +describe('HealthService Service', () => { + let service: HealthService; + let httpMock: HttpTestingController; + let applicationConfigService: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(HealthService); + applicationConfigService = TestBed.inject(ApplicationConfigService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should call management/health endpoint with correct values', () => { + // GIVEN + let expectedResult; + const checkHealth = { + components: [], + }; + + // WHEN + service.checkHealth().subscribe(received => { + expectedResult = received; + }); + const testRequest = httpMock.expectOne({ + method: 'GET', + url: applicationConfigService.getEndpointFor('management/health'), + }); + testRequest.flush(checkHealth); + + // THEN + expect(expectedResult).toEqual(checkHealth); + }); + }); +}); diff --git a/src/main/webapp/app/admin/health/health.service.ts b/src/main/webapp/app/admin/health/health.service.ts new file mode 100644 index 0000000..c62fd83 --- /dev/null +++ b/src/main/webapp/app/admin/health/health.service.ts @@ -0,0 +1,16 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { Health } from './health.model'; + +@Injectable({ providedIn: 'root' }) +export class HealthService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + checkHealth(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('management/health')); + } +} diff --git a/src/main/webapp/app/admin/health/modal/health-modal.component.html b/src/main/webapp/app/admin/health/modal/health-modal.component.html new file mode 100644 index 0000000..4039cb9 --- /dev/null +++ b/src/main/webapp/app/admin/health/modal/health-modal.component.html @@ -0,0 +1,51 @@ + + + + + diff --git a/src/main/webapp/app/admin/health/modal/health-modal.component.spec.ts b/src/main/webapp/app/admin/health/modal/health-modal.component.spec.ts new file mode 100644 index 0000000..b9aa047 --- /dev/null +++ b/src/main/webapp/app/admin/health/modal/health-modal.component.spec.ts @@ -0,0 +1,111 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import HealthModalComponent from './health-modal.component'; + +describe('HealthModalComponent', () => { + let comp: HealthModalComponent; + let fixture: ComponentFixture; + let mockActiveModal: NgbActiveModal; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, HealthModalComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(HealthModalComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthModalComponent); + comp = fixture.componentInstance; + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('readableValue', () => { + it('should return stringify value', () => { + // GIVEN + comp.health = undefined; + + // WHEN + const result = comp.readableValue({ name: 'jhipster' }); + + // THEN + expect(result).toEqual('{"name":"jhipster"}'); + }); + + it('should return string value', () => { + // GIVEN + comp.health = undefined; + + // WHEN + const result = comp.readableValue('jhipster'); + + // THEN + expect(result).toEqual('jhipster'); + }); + + it('should return storage space in an human readable unit (GB)', () => { + // GIVEN + comp.health = { + key: 'diskSpace', + value: { + status: 'UP', + }, + }; + + // WHEN + const result = comp.readableValue(1073741825); + + // THEN + expect(result).toEqual('1.00 GB'); + }); + + it('should return storage space in an human readable unit (MB)', () => { + // GIVEN + comp.health = { + key: 'diskSpace', + value: { + status: 'UP', + }, + }; + + // WHEN + const result = comp.readableValue(1073741824); + + // THEN + expect(result).toEqual('1024.00 MB'); + }); + + it('should return string value', () => { + // GIVEN + comp.health = { + key: 'mail', + value: { + status: 'UP', + }, + }; + + // WHEN + const result = comp.readableValue(1234); + + // THEN + expect(result).toEqual('1234'); + }); + }); + + describe('dismiss', () => { + it('should call dismiss when dismiss modal is called', () => { + // GIVEN + const spy = jest.spyOn(mockActiveModal, 'dismiss'); + + // WHEN + comp.dismiss(); + + // THEN + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/admin/health/modal/health-modal.component.ts b/src/main/webapp/app/admin/health/modal/health-modal.component.ts new file mode 100644 index 0000000..e816a75 --- /dev/null +++ b/src/main/webapp/app/admin/health/modal/health-modal.component.ts @@ -0,0 +1,37 @@ +import { Component, inject } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { HealthKey, HealthDetails } from '../health.model'; + +@Component({ + standalone: true, + selector: 'jhi-health-modal', + templateUrl: './health-modal.component.html', + imports: [SharedModule], +}) +export default class HealthModalComponent { + health?: { key: HealthKey; value: HealthDetails }; + + private activeModal = inject(NgbActiveModal); + + readableValue(value: any): string { + if (this.health?.key === 'diskSpace') { + // Should display storage space in an human readable unit + const val = value / 1073741824; + if (val > 1) { + return `${val.toFixed(2)} GB`; + } + return `${(value / 1048576).toFixed(2)} MB`; + } + + if (typeof value === 'object') { + return JSON.stringify(value); + } + return String(value); + } + + dismiss(): void { + this.activeModal.dismiss(); + } +} diff --git a/src/main/webapp/app/admin/metrics/blocks/jvm-memory/jvm-memory.component.html b/src/main/webapp/app/admin/metrics/blocks/jvm-memory/jvm-memory.component.html new file mode 100644 index 0000000..233af58 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/jvm-memory/jvm-memory.component.html @@ -0,0 +1,25 @@ +

Memória

+ +@if (!updating() && jvmMemoryMetrics()) { +
+ @for (entry of jvmMemoryMetrics() | keyvalue; track $index) { +
+ @if (entry.value.max !== -1) { + + {{ entry.key }} + ({{ entry.value.used / 1048576 | number: '1.0-0' }}M / {{ entry.value.max / 1048576 | number: '1.0-0' }}M) + + +
Committed : {{ entry.value.committed / 1048576 | number: '1.0-0' }}M
+ + {{ (entry.value.used * 100) / entry.value.max | number: '1.0-0' }}% + + } @else { + {{ entry.key }} {{ entry.value.used / 1048576 | number: '1.0-0' }}M + } +
+ } +
+} diff --git a/src/main/webapp/app/admin/metrics/blocks/jvm-memory/jvm-memory.component.ts b/src/main/webapp/app/admin/metrics/blocks/jvm-memory/jvm-memory.component.ts new file mode 100644 index 0000000..d01f68d --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/jvm-memory/jvm-memory.component.ts @@ -0,0 +1,22 @@ +import { Component, input } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { JvmMetrics } from 'app/admin/metrics/metrics.model'; + +@Component({ + standalone: true, + selector: 'jhi-jvm-memory', + templateUrl: './jvm-memory.component.html', + imports: [SharedModule], +}) +export class JvmMemoryComponent { + /** + * object containing all jvm memory metrics + */ + jvmMemoryMetrics = input<{ [key: string]: JvmMetrics }>(); + + /** + * boolean field saying if the metrics are in the process of being updated + */ + updating = input(); +} diff --git a/src/main/webapp/app/admin/metrics/blocks/jvm-threads/jvm-threads.component.html b/src/main/webapp/app/admin/metrics/blocks/jvm-threads/jvm-threads.component.html new file mode 100644 index 0000000..b53184c --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/jvm-threads/jvm-threads.component.html @@ -0,0 +1,55 @@ +

Threads

+ +Runnable {{ threadStats.threadDumpRunnable }} + + + {{ (threadStats.threadDumpRunnable * 100) / threadStats.threadDumpAll | number: '1.0-0' }}% + + +Tempo de espera ({{ threadStats.threadDumpTimedWaiting }}) + + + {{ (threadStats.threadDumpTimedWaiting * 100) / threadStats.threadDumpAll | number: '1.0-0' }}% + + +Aguardando ({{ threadStats.threadDumpWaiting }}) + + + {{ (threadStats.threadDumpWaiting * 100) / threadStats.threadDumpAll | number: '1.0-0' }}% + + +Bloqueado ({{ threadStats.threadDumpBlocked }}) + + + {{ (threadStats.threadDumpBlocked * 100) / threadStats.threadDumpAll | number: '1.0-0' }}% + + +
Total: {{ threadStats.threadDumpAll }}
+ + diff --git a/src/main/webapp/app/admin/metrics/blocks/jvm-threads/jvm-threads.component.ts b/src/main/webapp/app/admin/metrics/blocks/jvm-threads/jvm-threads.component.ts new file mode 100644 index 0000000..0aade91 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/jvm-threads/jvm-threads.component.ts @@ -0,0 +1,58 @@ +import { Component, inject, Input } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { Thread, ThreadState } from 'app/admin/metrics/metrics.model'; +import { MetricsModalThreadsComponent } from '../metrics-modal-threads/metrics-modal-threads.component'; + +@Component({ + standalone: true, + selector: 'jhi-jvm-threads', + templateUrl: './jvm-threads.component.html', + imports: [SharedModule], +}) +export class JvmThreadsComponent { + threadStats = { + threadDumpAll: 0, + threadDumpRunnable: 0, + threadDumpTimedWaiting: 0, + threadDumpWaiting: 0, + threadDumpBlocked: 0, + }; + + @Input() + set threads(threads: Thread[] | undefined) { + this._threads = threads; + + threads?.forEach(thread => { + if (thread.threadState === ThreadState.Runnable) { + this.threadStats.threadDumpRunnable += 1; + } else if (thread.threadState === ThreadState.Waiting) { + this.threadStats.threadDumpWaiting += 1; + } else if (thread.threadState === ThreadState.TimedWaiting) { + this.threadStats.threadDumpTimedWaiting += 1; + } else if (thread.threadState === ThreadState.Blocked) { + this.threadStats.threadDumpBlocked += 1; + } + }); + + this.threadStats.threadDumpAll = + this.threadStats.threadDumpRunnable + + this.threadStats.threadDumpWaiting + + this.threadStats.threadDumpTimedWaiting + + this.threadStats.threadDumpBlocked; + } + + get threads(): Thread[] | undefined { + return this._threads; + } + + private _threads: Thread[] | undefined; + + private modalService = inject(NgbModal); + + open(): void { + const modalRef = this.modalService.open(MetricsModalThreadsComponent); + modalRef.componentInstance.threads = this.threads; + } +} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-cache/metrics-cache.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-cache/metrics-cache.component.html new file mode 100644 index 0000000..ae114b9 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-cache/metrics-cache.component.html @@ -0,0 +1,46 @@ +

Estatísticas da cache

+ +@if (!updating() && cacheMetrics()) { +
+ + + + + + + + + + + + + + + + @for (entry of cacheMetrics() | keyvalue; track entry.key) { + + + + + + + + + + + + } + +
Nome da cacheHitsMissesCache GetsCache PutsCache RemovalsDespejosCache Hit %Cache Miss %
{{ entry.key }}{{ entry.value['cache.gets.hit'] }}{{ entry.value['cache.gets.miss'] }}{{ entry.value['cache.gets.hit'] + entry.value['cache.gets.miss'] }}{{ entry.value['cache.puts'] }}{{ entry.value['cache.removals'] }}{{ entry.value['cache.evictions'] }} + {{ + filterNaN((100 * entry.value['cache.gets.hit']) / (entry.value['cache.gets.hit'] + entry.value['cache.gets.miss'])) + | number: '1.0-4' + }} + + {{ + filterNaN((100 * entry.value['cache.gets.miss']) / (entry.value['cache.gets.hit'] + entry.value['cache.gets.miss'])) + | number: '1.0-4' + }} +
+
+} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-cache/metrics-cache.component.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-cache/metrics-cache.component.ts new file mode 100644 index 0000000..ce6e84f --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-cache/metrics-cache.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { CacheMetrics } from 'app/admin/metrics/metrics.model'; +import { filterNaN } from 'app/core/util/operators'; + +@Component({ + standalone: true, + selector: 'jhi-metrics-cache', + templateUrl: './metrics-cache.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SharedModule], +}) +export class MetricsCacheComponent { + /** + * object containing all cache related metrics + */ + cacheMetrics = input<{ [key: string]: CacheMetrics }>(); + + /** + * boolean field saying if the metrics are in the process of being updated + */ + updating = input(); + + filterNaN = (n: number): number => filterNaN(n); +} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-datasource/metrics-datasource.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-datasource/metrics-datasource.component.html new file mode 100644 index 0000000..4fc9b87 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-datasource/metrics-datasource.component.html @@ -0,0 +1,59 @@ +

Estatísticas da DataSource (tempo em milisegundos)

+ +@if (!updating() && datasourceMetrics()) { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Utilização (active: {{ datasourceMetrics()!.active.value }}, min: + {{ datasourceMetrics()!.min.value }}, max: {{ datasourceMetrics()!.max.value }}, idle: {{ datasourceMetrics()!.idle.value }}) + ContagemMedianaMinp50p75p95p99Max
Acquire{{ datasourceMetrics()!.acquire.count }}{{ filterNaN(datasourceMetrics()!.acquire.mean) | number: '1.0-2' }}{{ datasourceMetrics()!.acquire['0.0'] | number: '1.0-3' }}{{ datasourceMetrics()!.acquire['0.5'] | number: '1.0-3' }}{{ datasourceMetrics()!.acquire['0.75'] | number: '1.0-3' }}{{ datasourceMetrics()!.acquire['0.95'] | number: '1.0-3' }}{{ datasourceMetrics()!.acquire['0.99'] | number: '1.0-3' }}{{ filterNaN(datasourceMetrics()!.acquire.max) | number: '1.0-2' }}
Creation{{ datasourceMetrics()!.creation.count }}{{ filterNaN(datasourceMetrics()!.creation.mean) | number: '1.0-2' }}{{ datasourceMetrics()!.creation['0.0'] | number: '1.0-3' }}{{ datasourceMetrics()!.creation['0.5'] | number: '1.0-3' }}{{ datasourceMetrics()!.creation['0.75'] | number: '1.0-3' }}{{ datasourceMetrics()!.creation['0.95'] | number: '1.0-3' }}{{ datasourceMetrics()!.creation['0.99'] | number: '1.0-3' }}{{ filterNaN(datasourceMetrics()!.creation.max) | number: '1.0-2' }}
Usage{{ datasourceMetrics()!.usage.count }}{{ filterNaN(datasourceMetrics()!.usage.mean) | number: '1.0-2' }}{{ datasourceMetrics()!.usage['0.0'] | number: '1.0-3' }}{{ datasourceMetrics()!.usage['0.5'] | number: '1.0-3' }}{{ datasourceMetrics()!.usage['0.75'] | number: '1.0-3' }}{{ datasourceMetrics()!.usage['0.95'] | number: '1.0-3' }}{{ datasourceMetrics()!.usage['0.99'] | number: '1.0-3' }}{{ filterNaN(datasourceMetrics()!.usage.max) | number: '1.0-2' }}
+
+} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-datasource/metrics-datasource.component.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-datasource/metrics-datasource.component.ts new file mode 100644 index 0000000..ad7ada1 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-datasource/metrics-datasource.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { Databases } from 'app/admin/metrics/metrics.model'; +import { filterNaN } from 'app/core/util/operators'; + +@Component({ + standalone: true, + selector: 'jhi-metrics-datasource', + templateUrl: './metrics-datasource.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SharedModule], +}) +export class MetricsDatasourceComponent { + /** + * object containing all datasource related metrics + */ + datasourceMetrics = input(); + + /** + * boolean field saying if the metrics are in the process of being updated + */ + updating = input(); + + filterNaN = (n: number): number => filterNaN(n); +} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-endpoints-requests/metrics-endpoints-requests.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-endpoints-requests/metrics-endpoints-requests.component.html new file mode 100644 index 0000000..7cbc1fe --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-endpoints-requests/metrics-endpoints-requests.component.html @@ -0,0 +1,28 @@ +

Endpoints requests (time in millisecond)

+ +@if (!updating() && endpointsRequestsMetrics()) { +
+ + + + + + + + + + + @for (entry of endpointsRequestsMetrics() | keyvalue; track entry.key) { + @for (method of entry.value | keyvalue; track method.key) { + + + + + + + } + } + +
MethodEndpoint urlCountMean
{{ method.key }}{{ entry.key }}{{ method.value!.count }}{{ method.value!.mean | number: '1.0-3' }}
+
+} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-endpoints-requests/metrics-endpoints-requests.component.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-endpoints-requests/metrics-endpoints-requests.component.ts new file mode 100644 index 0000000..ce45c0e --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-endpoints-requests/metrics-endpoints-requests.component.ts @@ -0,0 +1,22 @@ +import { Component, input } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { Services } from 'app/admin/metrics/metrics.model'; + +@Component({ + standalone: true, + selector: 'jhi-metrics-endpoints-requests', + templateUrl: './metrics-endpoints-requests.component.html', + imports: [SharedModule], +}) +export class MetricsEndpointsRequestsComponent { + /** + * object containing service related metrics + */ + endpointsRequestsMetrics = input(); + + /** + * boolean field saying if the metrics are in the process of being updated + */ + updating = input(); +} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.html new file mode 100644 index 0000000..0564778 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.html @@ -0,0 +1,101 @@ +

Garbage collections

+ +
+
+ @if (garbageCollectorMetrics()) { +
+ + GC Live Data Size/GC Max Data Size ({{ garbageCollectorMetrics()!['jvm.gc.live.data.size'] / 1048576 | number: '1.0-0' }}M / + {{ garbageCollectorMetrics()!['jvm.gc.max.data.size'] / 1048576 | number: '1.0-0' }}M) + + + + + {{ + (100 * garbageCollectorMetrics()!['jvm.gc.live.data.size']) / garbageCollectorMetrics()!['jvm.gc.max.data.size'] + | number: '1.0-2' + }}% + + +
+ } +
+ +
+ @if (garbageCollectorMetrics()) { +
+ + GC Memory Promoted/GC Memory Allocated ({{ garbageCollectorMetrics()!['jvm.gc.memory.promoted'] / 1048576 | number: '1.0-0' }}M / + {{ garbageCollectorMetrics()!['jvm.gc.memory.allocated'] / 1048576 | number: '1.0-0' }}M) + + + + + {{ + (100 * garbageCollectorMetrics()!['jvm.gc.memory.promoted']) / garbageCollectorMetrics()!['jvm.gc.memory.allocated'] + | number: '1.0-2' + }}% + + +
+ } +
+ +
+ @if (garbageCollectorMetrics()) { +
+
Classes loaded
+
{{ garbageCollectorMetrics()!.classesLoaded }}
+
+
+
Classes unloaded
+
{{ garbageCollectorMetrics()!.classesUnloaded }}
+
+ } +
+ + @if (!updating() && garbageCollectorMetrics()) { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ContagemMedianaMinp50p75p95p99Max
jvm.gc.pause{{ garbageCollectorMetrics()!['jvm.gc.pause'].count }}{{ garbageCollectorMetrics()!['jvm.gc.pause'].mean | number: '1.0-3' }}{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.0'] | number: '1.0-3' }}{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.5'] | number: '1.0-3' }}{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.75'] | number: '1.0-3' }}{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.95'] | number: '1.0-3' }}{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.99'] | number: '1.0-3' }}{{ garbageCollectorMetrics()!['jvm.gc.pause'].max | number: '1.0-3' }}
+
+ } +
diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.ts new file mode 100644 index 0000000..7cdefa0 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-garbagecollector/metrics-garbagecollector.component.ts @@ -0,0 +1,22 @@ +import { Component, input } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { GarbageCollector } from 'app/admin/metrics/metrics.model'; + +@Component({ + standalone: true, + selector: 'jhi-metrics-garbagecollector', + templateUrl: './metrics-garbagecollector.component.html', + imports: [SharedModule], +}) +export class MetricsGarbageCollectorComponent { + /** + * object containing garbage collector related metrics + */ + garbageCollectorMetrics = input(); + + /** + * boolean field saying if the metrics are in the process of being updated + */ + updating = input(); +} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.html new file mode 100644 index 0000000..55e3daa --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.html @@ -0,0 +1,120 @@ + + + + diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.spec.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.spec.ts new file mode 100644 index 0000000..11326f7 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.spec.ts @@ -0,0 +1,325 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { ThreadState } from '../../metrics.model'; +import { MetricsModalThreadsComponent } from './metrics-modal-threads.component'; + +describe('MetricsModalThreadsComponent', () => { + let comp: MetricsModalThreadsComponent; + let fixture: ComponentFixture; + let mockActiveModal: NgbActiveModal; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetricsModalThreadsComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(MetricsModalThreadsComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MetricsModalThreadsComponent); + comp = fixture.componentInstance; + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('ngOnInit', () => { + it('should count threads on init', () => { + // GIVEN + comp.threads = [ + { + threadName: '', + threadId: 1, + blockedTime: 1, + blockedCount: 1, + waitedTime: 1, + waitedCount: 1, + lockName: 'lock1', + lockOwnerId: 1, + lockOwnerName: 'lock1', + daemon: true, + inNative: true, + suspended: true, + threadState: ThreadState.Blocked, + priority: 1, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }, + { + threadName: '', + threadId: 2, + blockedTime: 2, + blockedCount: 2, + waitedTime: 2, + waitedCount: 2, + lockName: 'lock2', + lockOwnerId: 2, + lockOwnerName: 'lock2', + daemon: false, + inNative: false, + suspended: false, + threadState: ThreadState.Runnable, + priority: 2, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }, + { + threadName: '', + threadId: 3, + blockedTime: 3, + blockedCount: 3, + waitedTime: 3, + waitedCount: 3, + lockName: 'lock3', + lockOwnerId: 3, + lockOwnerName: 'lock3', + daemon: false, + inNative: false, + suspended: false, + threadState: ThreadState.TimedWaiting, + priority: 3, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }, + { + threadName: '', + threadId: 4, + blockedTime: 4, + blockedCount: 4, + waitedTime: 4, + waitedCount: 4, + lockName: 'lock4', + lockOwnerId: 4, + lockOwnerName: 'lock4', + daemon: false, + inNative: false, + suspended: false, + threadState: ThreadState.Waiting, + priority: 4, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }, + ]; + + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.threadDumpRunnable).toEqual(1); + expect(comp.threadDumpWaiting).toEqual(1); + expect(comp.threadDumpTimedWaiting).toEqual(1); + expect(comp.threadDumpBlocked).toEqual(1); + expect(comp.threadDumpAll).toEqual(4); + }); + }); + + describe('getBadgeClass', () => { + it('should return a success badge class for runnable thread state', () => { + // GIVEN + const threadState = ThreadState.Runnable; + + // WHEN + const badgeClass = comp.getBadgeClass(threadState); + + // THEN + expect(badgeClass).toEqual('bg-success'); + }); + + it('should return an info badge class for waiting thread state', () => { + // GIVEN + const threadState = ThreadState.Waiting; + + // WHEN + const badgeClass = comp.getBadgeClass(threadState); + + // THEN + expect(badgeClass).toEqual('bg-info'); + }); + + it('should return a warning badge class for time waiting thread state', () => { + // GIVEN + const threadState = ThreadState.TimedWaiting; + + // WHEN + const badgeClass = comp.getBadgeClass(threadState); + + // THEN + expect(badgeClass).toEqual('bg-warning'); + }); + + it('should return a danger badge class for blocked thread state', () => { + // GIVEN + const threadState = ThreadState.Blocked; + + // WHEN + const badgeClass = comp.getBadgeClass(threadState); + + // THEN + expect(badgeClass).toEqual('bg-danger'); + }); + + it('should return an empty string for others threads', () => { + // GIVEN + const threadState = ThreadState.New; + + // WHEN + const badgeClass = comp.getBadgeClass(threadState); + + // THEN + expect(badgeClass).toEqual(''); + }); + }); + + describe('getThreads', () => { + it('should return blocked threads', () => { + // GIVEN + const thread1 = { + threadName: '', + threadId: 1, + blockedTime: 1, + blockedCount: 1, + waitedTime: 1, + waitedCount: 1, + lockName: 'lock1', + lockOwnerId: 1, + lockOwnerName: 'lock1', + daemon: true, + inNative: true, + suspended: true, + threadState: ThreadState.Blocked, + priority: 1, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }; + const thread2 = { + threadName: '', + threadId: 2, + blockedTime: 2, + blockedCount: 2, + waitedTime: 2, + waitedCount: 2, + lockName: 'lock2', + lockOwnerId: 1, + lockOwnerName: 'lock2', + daemon: false, + inNative: false, + suspended: false, + threadState: ThreadState.Runnable, + priority: 2, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }; + comp.threads = [thread1, thread2]; + comp.threadStateFilter = ThreadState.Blocked; + + // WHEN + const threadsFiltered = comp.getThreads(); + + // THEN + expect(threadsFiltered).toEqual([thread1]); + }); + + it('should return an empty array of threads', () => { + // GIVEN + comp.threads = []; + comp.threadStateFilter = ThreadState.Blocked; + + // WHEN + const threadsFiltered = comp.getThreads(); + + // THEN + expect(threadsFiltered).toEqual([]); + }); + + it('should return all threads if there is no filter', () => { + // GIVEN + const thread1 = { + threadName: '', + threadId: 1, + blockedTime: 1, + blockedCount: 1, + waitedTime: 1, + waitedCount: 1, + lockName: 'lock1', + lockOwnerId: 1, + lockOwnerName: 'lock1', + daemon: true, + inNative: true, + suspended: true, + threadState: ThreadState.Blocked, + priority: 1, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }; + const thread2 = { + threadName: '', + threadId: 2, + blockedTime: 2, + blockedCount: 2, + waitedTime: 2, + waitedCount: 2, + lockName: 'lock2', + lockOwnerId: 1, + lockOwnerName: 'lock2', + daemon: false, + inNative: false, + suspended: false, + threadState: ThreadState.Runnable, + priority: 2, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }; + comp.threads = [thread1, thread2]; + comp.threadStateFilter = undefined; + + // WHEN + const threadsFiltered = comp.getThreads(); + + // THEN + expect(threadsFiltered).toEqual(comp.threads); + }); + + it('should return an empty array if there are no threads to filter', () => { + // GIVEN + comp.threads = undefined; + comp.threadStateFilter = ThreadState.Blocked; + + // WHEN + const threadsFiltered = comp.getThreads(); + + // THEN + expect(threadsFiltered).toEqual([]); + }); + }); + + describe('dismiss', () => { + it('should call dismiss function for modal on dismiss', () => { + // GIVEN + jest.spyOn(mockActiveModal, 'dismiss').mockReturnValue(undefined); + + // WHEN + comp.dismiss(); + + // THEN + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.ts new file mode 100644 index 0000000..41479a7 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-modal-threads/metrics-modal-threads.component.ts @@ -0,0 +1,62 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { Thread, ThreadState } from 'app/admin/metrics/metrics.model'; + +@Component({ + standalone: true, + selector: 'jhi-thread-modal', + templateUrl: './metrics-modal-threads.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SharedModule], +}) +export class MetricsModalThreadsComponent implements OnInit { + ThreadState = ThreadState; + threadStateFilter?: ThreadState; + threads?: Thread[]; + threadDumpAll = 0; + threadDumpBlocked = 0; + threadDumpRunnable = 0; + threadDumpTimedWaiting = 0; + threadDumpWaiting = 0; + + private activeModal = inject(NgbActiveModal); + + ngOnInit(): void { + this.threads?.forEach(thread => { + if (thread.threadState === ThreadState.Runnable) { + this.threadDumpRunnable += 1; + } else if (thread.threadState === ThreadState.Waiting) { + this.threadDumpWaiting += 1; + } else if (thread.threadState === ThreadState.TimedWaiting) { + this.threadDumpTimedWaiting += 1; + } else if (thread.threadState === ThreadState.Blocked) { + this.threadDumpBlocked += 1; + } + }); + + this.threadDumpAll = this.threadDumpRunnable + this.threadDumpWaiting + this.threadDumpTimedWaiting + this.threadDumpBlocked; + } + + getBadgeClass(threadState: ThreadState): string { + if (threadState === ThreadState.Runnable) { + return 'bg-success'; + } else if (threadState === ThreadState.Waiting) { + return 'bg-info'; + } else if (threadState === ThreadState.TimedWaiting) { + return 'bg-warning'; + } else if (threadState === ThreadState.Blocked) { + return 'bg-danger'; + } + return ''; + } + + getThreads(): Thread[] { + return this.threads?.filter(thread => !this.threadStateFilter || thread.threadState === this.threadStateFilter) ?? []; + } + + dismiss(): void { + this.activeModal.dismiss(); + } +} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-request/metrics-request.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-request/metrics-request.component.html new file mode 100644 index 0000000..5628b4e --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-request/metrics-request.component.html @@ -0,0 +1,36 @@ +

Pedidos HTTP (eventos por segundo)

+ +@if (!updating() && requestMetrics()) { + + + + + + + + + + + @for (entry of requestMetrics()!['percode'] | keyvalue; track entry.key) { + + + + + + + } + +
CódigoContagemMedianaMax
{{ entry.key }} + + {{ entry.value.count }} + + + {{ filterNaN(entry.value.mean) | number: '1.0-2' }} + {{ entry.value.max | number: '1.0-2' }}
+} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-request/metrics-request.component.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-request/metrics-request.component.ts new file mode 100644 index 0000000..2ced74f --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-request/metrics-request.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { HttpServerRequests } from 'app/admin/metrics/metrics.model'; +import { filterNaN } from 'app/core/util/operators'; + +@Component({ + standalone: true, + selector: 'jhi-metrics-request', + templateUrl: './metrics-request.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SharedModule], +}) +export class MetricsRequestComponent { + /** + * object containing http request related metrics + */ + requestMetrics = input(); + + /** + * boolean field saying if the metrics are in the process of being updated + */ + updating = input(); + + filterNaN = (n: number): number => filterNaN(n); +} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-system/metrics-system.component.html b/src/main/webapp/app/admin/metrics/blocks/metrics-system/metrics-system.component.html new file mode 100644 index 0000000..bf08926 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-system/metrics-system.component.html @@ -0,0 +1,53 @@ +

System

+ +@if (!updating() && systemMetrics()) { + +
+
Uptime
+
{{ convertMillisecondsToDuration(systemMetrics()!['process.uptime']) }}
+
+ +
+
Start time
+
{{ systemMetrics()!['process.start.time'] | date: 'full' }}
+
+ +
+
Process CPU usage
+
{{ 100 * systemMetrics()!['process.cpu.usage'] | number: '1.0-2' }} %
+
+ + + {{ 100 * systemMetrics()!['process.cpu.usage'] | number: '1.0-2' }} % + + +
+
System CPU usage
+
{{ 100 * systemMetrics()!['system.cpu.usage'] | number: '1.0-2' }} %
+
+ + + {{ 100 * systemMetrics()!['system.cpu.usage'] | number: '1.0-2' }} % + + +
+
System CPU count
+
{{ systemMetrics()!['system.cpu.count'] }}
+
+ +
+
System 1m Load average
+
{{ systemMetrics()!['system.load.average.1m'] | number: '1.0-2' }}
+
+ +
+
Process files max
+
{{ systemMetrics()!['process.files.max'] | number: '1.0-0' }}
+
+ +
+
Process files open
+
{{ systemMetrics()!['process.files.open'] | number: '1.0-0' }}
+
+
+} diff --git a/src/main/webapp/app/admin/metrics/blocks/metrics-system/metrics-system.component.ts b/src/main/webapp/app/admin/metrics/blocks/metrics-system/metrics-system.component.ts new file mode 100644 index 0000000..e1bb980 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/blocks/metrics-system/metrics-system.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import SharedModule from 'app/shared/shared.module'; +import { ProcessMetrics } from 'app/admin/metrics/metrics.model'; + +@Component({ + standalone: true, + selector: 'jhi-metrics-system', + templateUrl: './metrics-system.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SharedModule], +}) +export class MetricsSystemComponent { + /** + * object containing thread related metrics + */ + systemMetrics = input(); + + /** + * boolean field saying if the metrics are in the process of being updated + */ + updating = input(); + + convertMillisecondsToDuration(ms: number): string { + const times = { + year: 31557600000, + month: 2629746000, + day: 86400000, + hour: 3600000, + minute: 60000, + second: 1000, + }; + let timeString = ''; + for (const [key, value] of Object.entries(times)) { + if (Math.floor(ms / value) > 0) { + let plural = ''; + if (Math.floor(ms / value) > 1) { + plural = 's'; + } + timeString += `${Math.floor(ms / value).toString()} ${key.toString()}${plural} `; + ms = ms - value * Math.floor(ms / value); + } + } + return timeString; + } +} diff --git a/src/main/webapp/app/admin/metrics/metrics.component.html b/src/main/webapp/app/admin/metrics/metrics.component.html new file mode 100644 index 0000000..51f2aa8 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/metrics.component.html @@ -0,0 +1,51 @@ +
+

+ Métricas da aplicação + + +

+ +

Métricas da JVM

+ + @if (metrics() && !updatingMetrics()) { +
+ + + + + +
+ } + + @if (metrics() && metricsKeyExists('garbageCollector')) { + + } + + @if (updatingMetrics()) { +
Atualização...
+ } + + @if (metrics() && metricsKeyExists('http.server.requests')) { + + } + + @if (metrics() && metricsKeyExists('services')) { + + } + + @if (metrics() && metricsKeyExists('cache')) { + + } + + @if (metrics() && metricsKeyExistsAndObjectNotEmpty('databases')) { + + } +
diff --git a/src/main/webapp/app/admin/metrics/metrics.component.spec.ts b/src/main/webapp/app/admin/metrics/metrics.component.spec.ts new file mode 100644 index 0000000..ee40520 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/metrics.component.spec.ts @@ -0,0 +1,146 @@ +import { ChangeDetectorRef } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; + +import MetricsComponent from './metrics.component'; +import { MetricsService } from './metrics.service'; +import { Metrics, Thread, ThreadDump } from './metrics.model'; + +describe('MetricsComponent', () => { + let comp: MetricsComponent; + let fixture: ComponentFixture; + let service: MetricsService; + let changeDetector: ChangeDetectorRef; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetricsComponent], + }) + .overrideTemplate(MetricsComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MetricsComponent); + comp = fixture.componentInstance; + service = TestBed.inject(MetricsService); + changeDetector = fixture.debugElement.injector.get(ChangeDetectorRef); + }); + + describe('refresh', () => { + it('should call refresh on init', () => { + // GIVEN + const metrics = { + garbageCollector: { + 'PS Scavenge': { + collectionCount: 0, + collectionTime: 0, + }, + 'PS MarkSweep': { + collectionCount: 0, + collectionTime: 0, + }, + }, + } as unknown as Metrics; + const threadDump = { threads: [{ threadName: 'thread 1' } as Thread] } as ThreadDump; + + jest.spyOn(service, 'getMetrics').mockReturnValue(of(metrics)); + jest.spyOn(service, 'threadDump').mockReturnValue(of(threadDump)); + jest.spyOn(changeDetector.constructor.prototype, 'markForCheck'); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.getMetrics).toHaveBeenCalled(); + expect(comp.metrics()).toEqual(metrics); + expect(comp.threads()).toEqual(threadDump.threads); + expect(comp.updatingMetrics()).toBeFalsy(); + expect(changeDetector.constructor.prototype.markForCheck).toHaveBeenCalled(); + }); + }); + + describe('metricsKeyExists', () => { + it('should check that metrics key exists', () => { + // GIVEN + comp.metrics.set({ + garbageCollector: { + 'PS Scavenge': { + collectionCount: 0, + collectionTime: 0, + }, + 'PS MarkSweep': { + collectionCount: 0, + collectionTime: 0, + }, + }, + } as unknown as Metrics); + + // WHEN + const garbageCollectorKeyExists = comp.metricsKeyExists('garbageCollector'); + + // THEN + expect(garbageCollectorKeyExists).toBeTruthy(); + }); + + it('should check that metrics key does not exist', () => { + // GIVEN + comp.metrics.set({ + garbageCollector: { + 'PS Scavenge': { + collectionCount: 0, + collectionTime: 0, + }, + 'PS MarkSweep': { + collectionCount: 0, + collectionTime: 0, + }, + }, + } as unknown as Metrics); + + // WHEN + const databasesCollectorKeyExists = comp.metricsKeyExists('databases'); + + // THEN + expect(databasesCollectorKeyExists).toBeFalsy(); + }); + }); + + describe('metricsKeyExistsAndObjectNotEmpty', () => { + it('should check that metrics key exists and is not empty', () => { + // GIVEN + comp.metrics.set({ + garbageCollector: { + 'PS Scavenge': { + collectionCount: 0, + collectionTime: 0, + }, + 'PS MarkSweep': { + collectionCount: 0, + collectionTime: 0, + }, + }, + } as unknown as Metrics); + + // WHEN + const garbageCollectorKeyExistsAndNotEmpty = comp.metricsKeyExistsAndObjectNotEmpty('garbageCollector'); + + // THEN + expect(garbageCollectorKeyExistsAndNotEmpty).toBeTruthy(); + }); + + it('should check that metrics key is empty', () => { + // GIVEN + comp.metrics.set({ + garbageCollector: {}, + } as Metrics); + + // WHEN + const garbageCollectorKeyEmpty = comp.metricsKeyExistsAndObjectNotEmpty('garbageCollector'); + + // THEN + expect(garbageCollectorKeyEmpty).toBeFalsy(); + }); + }); +}); diff --git a/src/main/webapp/app/admin/metrics/metrics.component.ts b/src/main/webapp/app/admin/metrics/metrics.component.ts new file mode 100644 index 0000000..3128242 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/metrics.component.ts @@ -0,0 +1,64 @@ +import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, inject, signal } from '@angular/core'; +import { combineLatest } from 'rxjs'; + +import SharedModule from 'app/shared/shared.module'; +import { MetricsService } from './metrics.service'; +import { Metrics, Thread } from './metrics.model'; +import { JvmMemoryComponent } from './blocks/jvm-memory/jvm-memory.component'; +import { JvmThreadsComponent } from './blocks/jvm-threads/jvm-threads.component'; +import { MetricsCacheComponent } from './blocks/metrics-cache/metrics-cache.component'; +import { MetricsDatasourceComponent } from './blocks/metrics-datasource/metrics-datasource.component'; +import { MetricsEndpointsRequestsComponent } from './blocks/metrics-endpoints-requests/metrics-endpoints-requests.component'; +import { MetricsGarbageCollectorComponent } from './blocks/metrics-garbagecollector/metrics-garbagecollector.component'; +import { MetricsModalThreadsComponent } from './blocks/metrics-modal-threads/metrics-modal-threads.component'; +import { MetricsRequestComponent } from './blocks/metrics-request/metrics-request.component'; +import { MetricsSystemComponent } from './blocks/metrics-system/metrics-system.component'; + +@Component({ + standalone: true, + selector: 'jhi-metrics', + templateUrl: './metrics.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + SharedModule, + JvmMemoryComponent, + JvmThreadsComponent, + MetricsCacheComponent, + MetricsDatasourceComponent, + MetricsEndpointsRequestsComponent, + MetricsGarbageCollectorComponent, + MetricsModalThreadsComponent, + MetricsRequestComponent, + MetricsSystemComponent, + ], +}) +export default class MetricsComponent implements OnInit { + metrics = signal(undefined); + threads = signal(undefined); + updatingMetrics = signal(true); + + private metricsService = inject(MetricsService); + private changeDetector = inject(ChangeDetectorRef); + + ngOnInit(): void { + this.refresh(); + } + + refresh(): void { + this.updatingMetrics.set(true); + combineLatest([this.metricsService.getMetrics(), this.metricsService.threadDump()]).subscribe(([metrics, threadDump]) => { + this.metrics.set(metrics); + this.threads.set(threadDump.threads); + this.updatingMetrics.set(false); + this.changeDetector.markForCheck(); + }); + } + + metricsKeyExists(key: keyof Metrics): boolean { + return Boolean(this.metrics()?.[key]); + } + + metricsKeyExistsAndObjectNotEmpty(key: keyof Metrics): boolean { + return Boolean(this.metrics()?.[key] && JSON.stringify(this.metrics()?.[key]) !== '{}'); + } +} diff --git a/src/main/webapp/app/admin/metrics/metrics.model.ts b/src/main/webapp/app/admin/metrics/metrics.model.ts new file mode 100644 index 0000000..059ce8c --- /dev/null +++ b/src/main/webapp/app/admin/metrics/metrics.model.ts @@ -0,0 +1,158 @@ +export interface Metrics { + jvm: { [key: string]: JvmMetrics }; + databases: Databases; + 'http.server.requests': HttpServerRequests; + cache: { [key: string]: CacheMetrics }; + garbageCollector: GarbageCollector; + services: Services; + processMetrics: ProcessMetrics; +} + +export interface JvmMetrics { + committed: number; + max: number; + used: number; +} + +export interface Databases { + min: Value; + idle: Value; + max: Value; + usage: MetricsWithPercentile; + pending: Value; + active: Value; + acquire: MetricsWithPercentile; + creation: MetricsWithPercentile; + connections: Value; +} + +export interface Value { + value: number; +} + +export interface MetricsWithPercentile { + '0.0': number; + '1.0': number; + max: number; + totalTime: number; + mean: number; + '0.5': number; + count: number; + '0.99': number; + '0.75': number; + '0.95': number; +} + +export interface HttpServerRequests { + all: { + count: number; + }; + percode: { [key: string]: MaxMeanCount }; +} + +export interface MaxMeanCount { + max: number; + mean: number; + count: number; +} + +export interface CacheMetrics { + 'cache.gets.miss': number; + 'cache.puts': number; + 'cache.gets.hit': number; + 'cache.removals': number; + 'cache.evictions': number; +} + +export interface GarbageCollector { + 'jvm.gc.max.data.size': number; + 'jvm.gc.pause': MetricsWithPercentile; + 'jvm.gc.memory.promoted': number; + 'jvm.gc.memory.allocated': number; + classesLoaded: number; + 'jvm.gc.live.data.size': number; + classesUnloaded: number; +} + +export interface Services { + [key: string]: { + [key in HttpMethod]?: MaxMeanCount; + }; +} + +export enum HttpMethod { + Post = 'POST', + Get = 'GET', + Put = 'PUT', + Patch = 'PATCH', + Delete = 'DELETE', +} + +export interface ProcessMetrics { + 'system.cpu.usage': number; + 'system.cpu.count': number; + 'system.load.average.1m'?: number; + 'process.cpu.usage': number; + 'process.files.max'?: number; + 'process.files.open'?: number; + 'process.start.time': number; + 'process.uptime': number; +} + +export interface ThreadDump { + threads: Thread[]; +} + +export interface Thread { + threadName: string; + threadId: number; + blockedTime: number; + blockedCount: number; + waitedTime: number; + waitedCount: number; + lockName: string | null; + lockOwnerId: number; + lockOwnerName: string | null; + daemon: boolean; + inNative: boolean; + suspended: boolean; + threadState: ThreadState; + priority: number; + stackTrace: StackTrace[]; + lockedMonitors: LockedMonitor[]; + lockedSynchronizers: string[]; + lockInfo: LockInfo | null; + // custom field for showing-hiding thread dump + showThreadDump?: boolean; +} + +export interface LockInfo { + className: string; + identityHashCode: number; +} + +export interface LockedMonitor { + className: string; + identityHashCode: number; + lockedStackDepth: number; + lockedStackFrame: StackTrace; +} + +export interface StackTrace { + classLoaderName: string | null; + moduleName: string | null; + moduleVersion: string | null; + methodName: string; + fileName: string; + lineNumber: number; + className: string; + nativeMethod: boolean; +} + +export enum ThreadState { + Runnable = 'RUNNABLE', + TimedWaiting = 'TIMED_WAITING', + Waiting = 'WAITING', + Blocked = 'BLOCKED', + New = 'NEW', +} diff --git a/src/main/webapp/app/admin/metrics/metrics.service.spec.ts b/src/main/webapp/app/admin/metrics/metrics.service.spec.ts new file mode 100644 index 0000000..468ebd5 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/metrics.service.spec.ts @@ -0,0 +1,81 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { MetricsService } from './metrics.service'; +import { ThreadDump, ThreadState } from './metrics.model'; + +describe('Logs Service', () => { + let service: MetricsService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + service = TestBed.inject(MetricsService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should return Metrics', () => { + let expectedResult; + const metrics = { + jvm: {}, + 'http.server.requests': {}, + cache: {}, + services: {}, + databases: {}, + garbageCollector: {}, + processMetrics: {}, + }; + + service.getMetrics().subscribe(received => { + expectedResult = received; + }); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(metrics); + expect(expectedResult).toEqual(metrics); + }); + + it('should return Thread Dump', () => { + let expectedResult: ThreadDump | null = null; + const dump: ThreadDump = { + threads: [ + { + threadName: 'Reference Handler', + threadId: 2, + blockedTime: -1, + blockedCount: 7, + waitedTime: -1, + waitedCount: 0, + lockName: null, + lockOwnerId: -1, + lockOwnerName: null, + daemon: true, + inNative: false, + suspended: false, + threadState: ThreadState.Runnable, + priority: 10, + stackTrace: [], + lockedMonitors: [], + lockedSynchronizers: [], + lockInfo: null, + }, + ], + }; + + service.threadDump().subscribe(received => { + expectedResult = received; + }); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(dump); + expect(expectedResult).toEqual(dump); + }); + }); +}); diff --git a/src/main/webapp/app/admin/metrics/metrics.service.ts b/src/main/webapp/app/admin/metrics/metrics.service.ts new file mode 100644 index 0000000..0f0fe51 --- /dev/null +++ b/src/main/webapp/app/admin/metrics/metrics.service.ts @@ -0,0 +1,20 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { Metrics, ThreadDump } from './metrics.model'; + +@Injectable({ providedIn: 'root' }) +export class MetricsService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + getMetrics(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('management/jhimetrics')); + } + + threadDump(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('management/threaddump')); + } +} diff --git a/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.html b/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.html new file mode 100644 index 0000000..37125cb --- /dev/null +++ b/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.html @@ -0,0 +1,25 @@ +@if (user) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.spec.ts b/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.spec.ts new file mode 100644 index 0000000..d4fc0a4 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.spec.ts @@ -0,0 +1,51 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, waitForAsync, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; + +import { UserManagementService } from '../service/user-management.service'; + +import UserManagementDeleteDialogComponent from './user-management-delete-dialog.component'; + +describe('User Management Delete Component', () => { + let comp: UserManagementDeleteDialogComponent; + let fixture: ComponentFixture; + let service: UserManagementService; + let mockActiveModal: NgbActiveModal; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UserManagementDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(UserManagementDeleteDialogComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserManagementDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UserManagementService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of({})); + + // WHEN + comp.confirmDelete('user'); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith('user'); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + }); +}); diff --git a/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.ts b/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.ts new file mode 100644 index 0000000..28ce8e3 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/delete/user-management-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { User } from '../user-management.model'; +import { UserManagementService } from '../service/user-management.service'; + +@Component({ + standalone: true, + selector: 'jhi-user-mgmt-delete-dialog', + templateUrl: './user-management-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export default class UserManagementDeleteDialogComponent { + user?: User; + + private userService = inject(UserManagementService); + private activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(login: string): void { + this.userService.delete(login).subscribe(() => { + this.activeModal.close('deleted'); + }); + } +} diff --git a/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.html b/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.html new file mode 100644 index 0000000..56d514b --- /dev/null +++ b/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.html @@ -0,0 +1,63 @@ +
+
+ @if (user()) { +
+

+ Utilizador [{{ user()!.login }}] +

+ +
+
Login
+
+ {{ user()!.login }} + @if (user()!.activated) { + Ativo + } @else { + Inativo + } +
+ +
Primeiro nome
+
{{ user()!.firstName }}
+ +
Último nome
+
{{ user()!.lastName }}
+ +
Email
+
{{ user()!.email }}
+ +
Linguagem
+
{{ user()!.langKey }}
+ +
Criado por
+
{{ user()!.createdBy }}
+ +
Criado em
+
{{ user()!.createdDate | date: 'dd/MM/yy HH:mm' }}
+ +
Alterado por
+
{{ user()!.lastModifiedBy }}
+ +
Alterado em
+
{{ user()!.lastModifiedDate | date: 'dd/MM/yy HH:mm' }}
+ +
Perfis
+
+
    + @for (authority of user()!.authorities; track $index) { +
  • + {{ authority }} +
  • + } +
+
+
+ + +
+ } +
+
diff --git a/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.spec.ts b/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.spec.ts new file mode 100644 index 0000000..2c234fe --- /dev/null +++ b/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.spec.ts @@ -0,0 +1,66 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { Authority } from 'app/config/authority.constants'; + +import UserManagementDetailComponent from './user-management-detail.component'; + +describe('User Management Detail Component', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserManagementDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: UserManagementDetailComponent, + resolve: { + user: () => + of({ + id: 123, + login: 'user', + firstName: 'first', + lastName: 'last', + email: 'first@last.com', + activated: true, + langKey: 'en', + authorities: [Authority.USER], + createdBy: 'admin', + }), + }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(UserManagementDetailComponent, '') + .compileComponents(); + }); + + describe('Construct', () => { + it('Should call load all on construct', async () => { + // WHEN + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', UserManagementDetailComponent); + + // THEN + expect(instance.user()).toEqual( + expect.objectContaining({ + id: 123, + login: 'user', + firstName: 'first', + lastName: 'last', + email: 'first@last.com', + activated: true, + langKey: 'en', + authorities: [Authority.USER], + createdBy: 'admin', + }), + ); + }); + }); +}); diff --git a/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.ts b/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.ts new file mode 100644 index 0000000..bd909a3 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/detail/user-management-detail.component.ts @@ -0,0 +1,15 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import SharedModule from 'app/shared/shared.module'; + +import { User } from '../user-management.model'; + +@Component({ + standalone: true, + selector: 'jhi-user-mgmt-detail', + templateUrl: './user-management-detail.component.html', + imports: [RouterModule, SharedModule], +}) +export default class UserManagementDetailComponent { + user = input(null); +} diff --git a/src/main/webapp/app/admin/user-management/list/user-management.component.html b/src/main/webapp/app/admin/user-management/list/user-management.component.html new file mode 100644 index 0000000..2ec5d33 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/list/user-management.component.html @@ -0,0 +1,132 @@ +
+

+ Utilizadores + +
+ + +
+

+ + + + + + @if (users()) { +
+ + + + + + + + + + + + + + + + + @for (user of users(); track trackIdentity) { + + + + + + + + + + + + + } + +
Código Login Email + Linguagem + Perfis + Criado em + + Alterado por + + Alterado em +
+ {{ user.id }} + {{ user.login }}{{ user.email }} + @if (!user.activated) { + + } @else { + + } + {{ user.langKey }} + @for (authority of user.authorities; track $index) { +
+ {{ authority }} +
+ } +
{{ user.createdDate | date: 'dd/MM/yy HH:mm' }}{{ user.lastModifiedBy }}{{ user.lastModifiedDate | date: 'dd/MM/yy HH:mm' }} +
+ + + + + +
+
+
+ +
+
+ +
+ +
+ +
+
+ } +
diff --git a/src/main/webapp/app/admin/user-management/list/user-management.component.spec.ts b/src/main/webapp/app/admin/user-management/list/user-management.component.spec.ts new file mode 100644 index 0000000..4af4244 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/list/user-management.component.spec.ts @@ -0,0 +1,102 @@ +jest.mock('app/core/auth/account.service'); + +import { ComponentFixture, TestBed, waitForAsync, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; +import { UserManagementService } from '../service/user-management.service'; +import { User } from '../user-management.model'; + +import UserManagementComponent from './user-management.component'; + +describe('User Management Component', () => { + let comp: UserManagementComponent; + let fixture: ComponentFixture; + let service: UserManagementService; + let mockAccountService: AccountService; + const data = of({ + defaultSort: 'id,asc', + }); + const queryParamMap = of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UserManagementComponent], + providers: [{ provide: ActivatedRoute, useValue: { data, queryParamMap } }, AccountService], + }) + .overrideTemplate(UserManagementComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserManagementComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UserManagementService); + mockAccountService = TestBed.inject(AccountService); + mockAccountService.identity = jest.fn(() => of(null)); + }); + + describe('OnInit', () => { + it('Should call load all on init', inject( + [], + fakeAsync(() => { + // GIVEN + const headers = new HttpHeaders().append('link', 'link;link'); + jest.spyOn(service, 'query').mockReturnValue( + of( + new HttpResponse({ + body: [new User(123)], + headers, + }), + ), + ); + + // WHEN + comp.ngOnInit(); + tick(); // simulate async + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.users()?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }), + )); + }); + + describe('setActive', () => { + it('Should update user and call load all', inject( + [], + fakeAsync(() => { + // GIVEN + const headers = new HttpHeaders().append('link', 'link;link'); + const user = new User(123); + jest.spyOn(service, 'query').mockReturnValue( + of( + new HttpResponse({ + body: [user], + headers, + }), + ), + ); + jest.spyOn(service, 'update').mockReturnValue(of(user)); + + // WHEN + comp.setActive(user, true); + tick(); // simulate async + + // THEN + expect(service.update).toHaveBeenCalledWith({ ...user, activated: true }); + expect(service.query).toHaveBeenCalled(); + expect(comp.users()?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }), + )); + }); +}); diff --git a/src/main/webapp/app/admin/user-management/list/user-management.component.ts b/src/main/webapp/app/admin/user-management/list/user-management.component.ts new file mode 100644 index 0000000..835acd4 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/list/user-management.component.ts @@ -0,0 +1,101 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { RouterModule, ActivatedRoute, Router } from '@angular/router'; +import { HttpResponse, HttpHeaders } from '@angular/common/http'; +import { combineLatest } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { SortDirective, SortByDirective, sortStateSignal, SortService, SortState } from 'app/shared/sort'; +import { ITEMS_PER_PAGE } from 'app/config/pagination.constants'; +import { SORT } from 'app/config/navigation.constants'; +import { ItemCountComponent } from 'app/shared/pagination'; +import { AccountService } from 'app/core/auth/account.service'; +import { UserManagementService } from '../service/user-management.service'; +import { User } from '../user-management.model'; +import UserManagementDeleteDialogComponent from '../delete/user-management-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-user-mgmt', + templateUrl: './user-management.component.html', + imports: [RouterModule, SharedModule, UserManagementDeleteDialogComponent, SortDirective, SortByDirective, ItemCountComponent], +}) +export default class UserManagementComponent implements OnInit { + currentAccount = inject(AccountService).trackCurrentAccount(); + users = signal(null); + isLoading = signal(false); + totalItems = signal(0); + itemsPerPage = ITEMS_PER_PAGE; + page!: number; + sortState = sortStateSignal({}); + + private userService = inject(UserManagementService); + private activatedRoute = inject(ActivatedRoute); + private router = inject(Router); + private sortService = inject(SortService); + private modalService = inject(NgbModal); + + ngOnInit(): void { + this.handleNavigation(); + } + + setActive(user: User, isActivated: boolean): void { + this.userService.update({ ...user, activated: isActivated }).subscribe(() => this.loadAll()); + } + + trackIdentity(_index: number, item: User): number { + return item.id!; + } + + deleteUser(user: User): void { + const modalRef = this.modalService.open(UserManagementDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.user = user; + // unsubscribe not needed because closed completes on modal close + modalRef.closed.subscribe(reason => { + if (reason === 'deleted') { + this.loadAll(); + } + }); + } + + loadAll(): void { + this.isLoading.set(true); + this.userService + .query({ + page: this.page - 1, + size: this.itemsPerPage, + sort: this.sortService.buildSortParam(this.sortState(), 'id'), + }) + .subscribe({ + next: (res: HttpResponse) => { + this.isLoading.set(false); + this.onSuccess(res.body, res.headers); + }, + error: () => this.isLoading.set(false), + }); + } + + transition(sortState?: SortState): void { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute.parent, + queryParams: { + page: this.page, + sort: this.sortService.buildSortParam(sortState ?? this.sortState()), + }, + }); + } + + private handleNavigation(): void { + combineLatest([this.activatedRoute.data, this.activatedRoute.queryParamMap]).subscribe(([data, params]) => { + const page = params.get('page'); + this.page = +(page ?? 1); + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data['defaultSort'])); + this.loadAll(); + }); + } + + private onSuccess(users: User[] | null, headers: HttpHeaders): void { + this.totalItems.set(Number(headers.get('X-Total-Count'))); + this.users.set(users); + } +} diff --git a/src/main/webapp/app/admin/user-management/service/user-management.service.spec.ts b/src/main/webapp/app/admin/user-management/service/user-management.service.spec.ts new file mode 100644 index 0000000..7fab783 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/service/user-management.service.spec.ts @@ -0,0 +1,67 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { Authority } from 'app/config/authority.constants'; +import { User } from '../user-management.model'; + +import { UserManagementService } from './user-management.service'; + +describe('User Service', () => { + let service: UserManagementService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(UserManagementService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should return User', () => { + let expectedResult: string | undefined; + + service.find('user').subscribe(received => { + expectedResult = received.login; + }); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(new User(123, 'user')); + expect(expectedResult).toEqual('user'); + }); + + it('should return Authorities', () => { + let expectedResult: string[] = []; + + service.authorities().subscribe(authorities => { + expectedResult = authorities; + }); + const req = httpMock.expectOne({ method: 'GET' }); + + req.flush([{ name: Authority.USER }, { name: Authority.ADMIN }]); + expect(expectedResult).toEqual([Authority.USER, Authority.ADMIN]); + }); + + it('should propagate not found response', () => { + let expectedResult = 0; + + service.find('user').subscribe({ + error: (error: HttpErrorResponse) => (expectedResult = error.status), + }); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush('Invalid request parameters', { + status: 404, + statusText: 'Bad Request', + }); + expect(expectedResult).toEqual(404); + }); + }); +}); diff --git a/src/main/webapp/app/admin/user-management/service/user-management.service.ts b/src/main/webapp/app/admin/user-management/service/user-management.service.ts new file mode 100644 index 0000000..7f732d2 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/service/user-management.service.ts @@ -0,0 +1,43 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { Pagination } from 'app/core/request/request.model'; +import { IUser } from '../user-management.model'; + +@Injectable({ providedIn: 'root' }) +export class UserManagementService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/admin/users'); + + create(user: IUser): Observable { + return this.http.post(this.resourceUrl, user); + } + + update(user: IUser): Observable { + return this.http.put(this.resourceUrl, user); + } + + find(login: string): Observable { + return this.http.get(`${this.resourceUrl}/${login}`); + } + + query(req?: Pagination): Observable> { + const options = createRequestOption(req); + return this.http.get(this.resourceUrl, { params: options, observe: 'response' }); + } + + delete(login: string): Observable<{}> { + return this.http.delete(`${this.resourceUrl}/${login}`); + } + + authorities(): Observable { + return this.http + .get>(this.applicationConfigService.getEndpointFor('api/authorities')) + .pipe(map(authorities => authorities.map(a => a.name))); + } +} diff --git a/src/main/webapp/app/admin/user-management/update/user-management-update.component.html b/src/main/webapp/app/admin/user-management/update/user-management-update.component.html new file mode 100644 index 0000000..59ce362 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/update/user-management-update.component.html @@ -0,0 +1,149 @@ +
+
+
+

Criar ou editar utilizador

+ + + +
+ + +
+ +
+ + + + @if (editForm.get('login')!.invalid && (editForm.get('login')!.dirty || editForm.get('login')!.touched)) { +
+ @if (editForm.get('login')?.errors?.required) { + O campo é obrigatório. + } + + @if (editForm.get('login')?.errors?.maxlength) { + Este campo não pode ter mais que 50 caracteres. + } + + @if (editForm.get('login')?.errors?.pattern) { + This field can only contain letters, digits and e-mail addresses. + } +
+ } +
+ +
+ + + + @if (editForm.get('firstName')!.invalid && (editForm.get('firstName')!.dirty || editForm.get('firstName')!.touched)) { +
+ @if (editForm.get('firstName')?.errors?.maxlength) { + Este campo não pode ter mais que 50 caracteres. + } +
+ } +
+ +
+ + + + @if (editForm.get('lastName')!.invalid && (editForm.get('lastName')!.dirty || editForm.get('lastName')!.touched)) { +
+ @if (editForm.get('lastName')?.errors?.maxlength) { + Este campo não pode ter mais que 50 caracteres. + } +
+ } +
+ +
+ + + + @if (editForm.get('email')!.invalid && (editForm.get('email')!.dirty || editForm.get('email')!.touched)) { +
+ @if (editForm.get('email')?.errors?.required) { + O campo é obrigatório. + } + + @if (editForm.get('email')?.errors?.maxlength) { + Este campo não pode ter mais que 100 caracteres. + } + + @if (editForm.get('email')?.errors?.minlength) { + Este campo deve ter pelo menos 5 caracteres. + } + + @if (editForm.get('email')?.errors?.email) { + Email inválido. + } +
+ } +
+ +
+ +
+ + @if (languages && languages.length > 0) { +
+ + +
+ } + +
+ + +
+ +
+ + +
+ + + + +
+
+
diff --git a/src/main/webapp/app/admin/user-management/update/user-management-update.component.spec.ts b/src/main/webapp/app/admin/user-management/update/user-management-update.component.spec.ts new file mode 100644 index 0000000..1e3a230 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/update/user-management-update.component.spec.ts @@ -0,0 +1,94 @@ +import { ComponentFixture, TestBed, waitForAsync, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; + +import { Authority } from 'app/config/authority.constants'; +import { UserManagementService } from '../service/user-management.service'; +import { User } from '../user-management.model'; + +import UserManagementUpdateComponent from './user-management-update.component'; + +describe('User Management Update Component', () => { + let comp: UserManagementUpdateComponent; + let fixture: ComponentFixture; + let service: UserManagementService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UserManagementUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + data: of({ user: new User(123, 'user', 'first', 'last', 'first@last.com', true, 'en', [Authority.USER], 'admin') }), + }, + }, + ], + }) + .overrideTemplate(UserManagementUpdateComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserManagementUpdateComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UserManagementService); + }); + + describe('OnInit', () => { + it('Should load authorities and language on init', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'authorities').mockReturnValue(of(['USER'])); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.authorities).toHaveBeenCalled(); + expect(comp.authorities()).toEqual(['USER']); + }), + )); + }); + + describe('save', () => { + it('Should call update service on save for existing user', inject( + [], + fakeAsync(() => { + // GIVEN + const entity = { id: 123 }; + jest.spyOn(service, 'update').mockReturnValue(of(entity)); + comp.editForm.patchValue(entity); + // WHEN + comp.save(); + tick(); // simulate async + + // THEN + expect(service.update).toHaveBeenCalledWith(expect.objectContaining(entity)); + expect(comp.isSaving()).toEqual(false); + }), + )); + + it('Should call create service on save for new user', inject( + [], + fakeAsync(() => { + // GIVEN + const entity = { login: 'foo' } as User; + jest.spyOn(service, 'create').mockReturnValue(of(entity)); + comp.editForm.patchValue(entity); + // WHEN + comp.save(); + tick(); // simulate async + + // THEN + expect(comp.editForm.getRawValue().id).toBeNull(); + expect(service.create).toHaveBeenCalledWith(expect.objectContaining(entity)); + expect(comp.isSaving()).toEqual(false); + }), + )); + }); +}); diff --git a/src/main/webapp/app/admin/user-management/update/user-management-update.component.ts b/src/main/webapp/app/admin/user-management/update/user-management-update.component.ts new file mode 100644 index 0000000..b9cf9cc --- /dev/null +++ b/src/main/webapp/app/admin/user-management/update/user-management-update.component.ts @@ -0,0 +1,111 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { FormGroup, FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { LANGUAGES } from 'app/config/language.constants'; +import { IUser } from '../user-management.model'; +import { UserManagementService } from '../service/user-management.service'; +import { ISecurityGroup } from 'app/security/security-group/security-group.model'; +import { SecurityGroupService } from 'app/security/security-group/service/security-group.service'; +import { map } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; + +const userTemplate = {} as IUser; + +const newUser: IUser = { + langKey: 'pt-pt', + activated: true, +} as IUser; + +@Component({ + standalone: true, + selector: 'jhi-user-mgmt-update', + templateUrl: './user-management-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export default class UserManagementUpdateComponent implements OnInit { + languages = LANGUAGES; + authorities = signal([]); + isSaving = signal(false); + + sharedSecurityGroupCollection: ISecurityGroup[] = []; + + editForm = new FormGroup({ + id: new FormControl(userTemplate.id), + login: new FormControl(userTemplate.login, { + nonNullable: true, + validators: [ + Validators.required, + Validators.minLength(1), + Validators.maxLength(50), + Validators.pattern('^[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$|^[_.@A-Za-z0-9-]+$'), + ], + }), + firstName: new FormControl(userTemplate.firstName, { validators: [Validators.maxLength(50)] }), + lastName: new FormControl(userTemplate.lastName, { validators: [Validators.maxLength(50)] }), + email: new FormControl(userTemplate.email, { + nonNullable: true, + validators: [Validators.minLength(5), Validators.maxLength(254), Validators.email], + }), + activated: new FormControl(userTemplate.activated, { nonNullable: true }), + langKey: new FormControl(userTemplate.langKey, { nonNullable: true }), + authorities: new FormControl(userTemplate.authorities, { nonNullable: true }), + securityGroup: new FormControl(userTemplate.securityGroup, { nonNullable: true }), + }); + + private userService = inject(UserManagementService); + private securityGroupService = inject(SecurityGroupService); + private route = inject(ActivatedRoute); + + compareSecurityGroup = (o1: ISecurityGroup | null, o2: ISecurityGroup | null): boolean => + this.securityGroupService.compareEntity(o1, o2); + + ngOnInit(): void { + this.route.data.subscribe(({ user }) => { + if (user) { + this.editForm.reset(user); + } else { + this.editForm.reset(newUser); + } + }); + this.userService.authorities().subscribe(authorities => this.authorities.set(authorities)); + this.loadSecurityGroups(); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving.set(true); + const user = this.editForm.getRawValue(); + if (user.id !== null) { + this.userService.update(user).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } else { + this.userService.create(user).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + } + + private onSaveSuccess(): void { + this.isSaving.set(false); + this.previousState(); + } + + private onSaveError(): void { + this.isSaving.set(false); + } + + private loadSecurityGroups(): void { + this.securityGroupService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((groups: ISecurityGroup[]) => (this.sharedSecurityGroupCollection = groups)); + } +} diff --git a/src/main/webapp/app/admin/user-management/user-management.model.ts b/src/main/webapp/app/admin/user-management/user-management.model.ts new file mode 100644 index 0000000..0cffa49 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/user-management.model.ts @@ -0,0 +1,34 @@ +import { ISecurityGroup } from "app/security/security-group/security-group.model"; + +export interface IUser { + id: number | null; + login?: string; + firstName?: string | null; + lastName?: string | null; + email?: string; + activated?: boolean; + langKey?: string; + authorities?: string[]; + createdBy?: string; + createdDate?: Date; + lastModifiedBy?: string; + lastModifiedDate?: Date; + securityGroup?: ISecurityGroup; +} + +export class User implements IUser { + constructor( + public id: number | null, + public login?: string, + public firstName?: string | null, + public lastName?: string | null, + public email?: string, + public activated?: boolean, + public langKey?: string, + public authorities?: string[], + public createdBy?: string, + public createdDate?: Date, + public lastModifiedBy?: string, + public lastModifiedDate?: Date, + ) {} +} diff --git a/src/main/webapp/app/admin/user-management/user-management.route.ts b/src/main/webapp/app/admin/user-management/user-management.route.ts new file mode 100644 index 0000000..1a8f764 --- /dev/null +++ b/src/main/webapp/app/admin/user-management/user-management.route.ts @@ -0,0 +1,50 @@ +import { inject } from '@angular/core'; +import { ActivatedRouteSnapshot, Routes, ResolveFn } from '@angular/router'; +import { of } from 'rxjs'; + +import { IUser } from './user-management.model'; +import { UserManagementService } from './service/user-management.service'; +import UserManagementComponent from './list/user-management.component'; +import UserManagementDetailComponent from './detail/user-management-detail.component'; +import UserManagementUpdateComponent from './update/user-management-update.component'; + +export const UserManagementResolve: ResolveFn = (route: ActivatedRouteSnapshot) => { + const login = route.paramMap.get('login'); + if (login) { + return inject(UserManagementService).find(login); + } + return of(null); +}; + +const userManagementRoute: Routes = [ + { + path: '', + component: UserManagementComponent, + data: { + defaultSort: 'id,asc', + }, + }, + { + path: ':login/view', + component: UserManagementDetailComponent, + resolve: { + user: UserManagementResolve, + }, + }, + { + path: 'new', + component: UserManagementUpdateComponent, + resolve: { + user: UserManagementResolve, + }, + }, + { + path: ':login/edit', + component: UserManagementUpdateComponent, + resolve: { + user: UserManagementResolve, + }, + }, +]; + +export default userManagementRoute; diff --git a/src/main/webapp/app/app-page-title-strategy.ts b/src/main/webapp/app/app-page-title-strategy.ts new file mode 100644 index 0000000..6c94059 --- /dev/null +++ b/src/main/webapp/app/app-page-title-strategy.ts @@ -0,0 +1,18 @@ +import { Injectable, inject } from '@angular/core'; +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable() +export class AppPageTitleStrategy extends TitleStrategy { + private translateService = inject(TranslateService); + + override updateTitle(routerState: RouterStateSnapshot): void { + let pageTitle = this.buildTitle(routerState); + if (!pageTitle) { + pageTitle = 'global.title'; + } + this.translateService.get(pageTitle).subscribe(title => { + document.title = title; + }); + } +} diff --git a/src/main/webapp/app/app.component.ts b/src/main/webapp/app/app.component.ts new file mode 100644 index 0000000..aa475d3 --- /dev/null +++ b/src/main/webapp/app/app.component.ts @@ -0,0 +1,33 @@ +import { Component, inject } from '@angular/core'; +import { registerLocaleData } from '@angular/common'; +import dayjs from 'dayjs/esm'; +import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import { NgbDatepickerConfig } from '@ng-bootstrap/ng-bootstrap'; +import locale from '@angular/common/locales/pt-PT'; +// jhipster-needle-angular-add-module-import JHipster will add new module here + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { fontAwesomeIcons } from './config/font-awesome-icons'; +import MainComponent from './layouts/main/main.component'; + +@Component({ + standalone: true, + selector: 'jhi-app', + template: '', + imports: [ + MainComponent, + // jhipster-needle-angular-add-module JHipster will add new module here + ], +}) +export default class AppComponent { + private applicationConfigService = inject(ApplicationConfigService); + private iconLibrary = inject(FaIconLibrary); + private dpConfig = inject(NgbDatepickerConfig); + + constructor() { + this.applicationConfigService.setEndpointPrefix(SERVER_API_URL); + registerLocaleData(locale); + this.iconLibrary.addIcons(...fontAwesomeIcons); + this.dpConfig.minDate = { year: dayjs().subtract(100, 'year').year(), month: 1, day: 1 }; + } +} diff --git a/src/main/webapp/app/app.config.ts b/src/main/webapp/app/app.config.ts new file mode 100644 index 0000000..3f76752 --- /dev/null +++ b/src/main/webapp/app/app.config.ts @@ -0,0 +1,64 @@ +import { ApplicationConfig, LOCALE_ID, importProvidersFrom, inject } from '@angular/core'; +import { BrowserModule, Title } from '@angular/platform-browser'; +import { + Router, + RouterFeatures, + TitleStrategy, + provideRouter, + withComponentInputBinding, + withDebugTracing, + withNavigationErrorHandler, + NavigationError, +} from '@angular/router'; +import { ServiceWorkerModule } from '@angular/service-worker'; +import { HttpClientModule } from '@angular/common/http'; + +import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; + +import { DEBUG_INFO_ENABLED } from 'app/app.constants'; +import './config/dayjs'; +import { TranslationModule } from 'app/shared/language/translation.module'; +import { httpInterceptorProviders } from './core/interceptor'; +import routes from './app.routes'; +// jhipster-needle-angular-add-module-import JHipster will add new module here +import { NgbDateDayjsAdapter } from './config/datepicker-adapter'; +import { AppPageTitleStrategy } from './app-page-title-strategy'; + +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +const routerFeatures: Array = [ + withComponentInputBinding(), + withNavigationErrorHandler((e: NavigationError) => { + const router = inject(Router); + if (e.error.status === 403) { + router.navigate(['/accessdenied']); + } else if (e.error.status === 404) { + router.navigate(['/404']); + } else if (e.error.status === 401) { + router.navigate(['/login']); + } else { + router.navigate(['/error']); + } + }), +]; +if (DEBUG_INFO_ENABLED) { + routerFeatures.push(withDebugTracing()); +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes, ...routerFeatures), + importProvidersFrom(BrowserModule), + importProvidersFrom(BrowserAnimationsModule), + // Set this to true to enable service worker (PWA) + importProvidersFrom(ServiceWorkerModule.register('ngsw-worker.js', { enabled: false })), + importProvidersFrom(TranslationModule), + importProvidersFrom(HttpClientModule), + Title, + { provide: LOCALE_ID, useValue: 'pt-PT' }, + { provide: NgbDateAdapter, useClass: NgbDateDayjsAdapter }, + httpInterceptorProviders, + { provide: TitleStrategy, useClass: AppPageTitleStrategy }, + // jhipster-needle-angular-add-module JHipster will add new module here + ], +}; diff --git a/src/main/webapp/app/app.constants.ts b/src/main/webapp/app/app.constants.ts new file mode 100644 index 0000000..695015c --- /dev/null +++ b/src/main/webapp/app/app.constants.ts @@ -0,0 +1,9 @@ +// These constants are injected via webpack DefinePlugin variables. +// You can add more variables in webpack.common.js or in profile specific webpack..js files. +// If you change the values in the webpack config files, you need to re run webpack to update the application + +declare const __DEBUG_INFO_ENABLED__: boolean; +declare const __VERSION__: string; + +export const VERSION = __VERSION__; +export const DEBUG_INFO_ENABLED = __DEBUG_INFO_ENABLED__; diff --git a/src/main/webapp/app/app.routes.ts b/src/main/webapp/app/app.routes.ts new file mode 100644 index 0000000..4d88813 --- /dev/null +++ b/src/main/webapp/app/app.routes.ts @@ -0,0 +1,55 @@ +import { Routes } from '@angular/router'; + +import { Authority } from 'app/config/authority.constants'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { errorRoute } from './layouts/error/error.route'; + +import HomeComponent from './home/home.component'; +import NavbarComponent from './layouts/navbar/navbar.component'; +import LoginComponent from './login/login.component'; + +const routes: Routes = [ + { + path: '', + component: HomeComponent, + title: 'home.title', + /* canActivate: [UserRouteAccessService], // Added to guarantee auto-redirect to /login if NOT authenticated */ + }, + { + path: '', + component: NavbarComponent, + outlet: 'navbar', + }, + { + path: 'admin', + data: { + authorities: [Authority.ADMIN], + }, + canActivate: [UserRouteAccessService], + loadChildren: () => import('./admin/admin.routes'), + }, + { + path: 'security', + data: { + authorities: [Authority.ADMIN], + }, + canActivate: [UserRouteAccessService], + loadChildren: () => import('./security/security.routes'), + }, + { + path: 'account', + loadChildren: () => import('./account/account.route'), + }, + { + path: 'login', + component: LoginComponent, + title: 'login.title', + }, + { + path: '', + loadChildren: () => import(`./entities/entity.routes`), + }, + ...errorRoute, +]; + +export default routes; diff --git a/src/main/webapp/app/config/authority.constants.ts b/src/main/webapp/app/config/authority.constants.ts new file mode 100644 index 0000000..911328f --- /dev/null +++ b/src/main/webapp/app/config/authority.constants.ts @@ -0,0 +1,6 @@ +export enum Authority { + ADMIN = 'ROLE_ADMIN', + MANAGER = 'ROLE_MANAGER', + COORDINATOR = 'ROLE_COORDINATOR', + USER = 'ROLE_USER', +} diff --git a/src/main/webapp/app/config/datepicker-adapter.ts b/src/main/webapp/app/config/datepicker-adapter.ts new file mode 100644 index 0000000..3f8b16c --- /dev/null +++ b/src/main/webapp/app/config/datepicker-adapter.ts @@ -0,0 +1,20 @@ +/** + * Angular bootstrap Date adapter + */ +import { Injectable } from '@angular/core'; +import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import dayjs from 'dayjs/esm'; + +@Injectable() +export class NgbDateDayjsAdapter extends NgbDateAdapter { + fromModel(date: dayjs.Dayjs | null): NgbDateStruct | null { + if (date && dayjs.isDayjs(date) && date.isValid()) { + return { year: date.year(), month: date.month() + 1, day: date.date() }; + } + return null; + } + + toModel(date: NgbDateStruct | null): dayjs.Dayjs | null { + return date ? dayjs(`${date.year}-${date.month}-${date.day}`) : null; + } +} diff --git a/src/main/webapp/app/config/dayjs.ts b/src/main/webapp/app/config/dayjs.ts new file mode 100644 index 0000000..62e3e24 --- /dev/null +++ b/src/main/webapp/app/config/dayjs.ts @@ -0,0 +1,16 @@ +import dayjs from 'dayjs/esm'; +import customParseFormat from 'dayjs/esm/plugin/customParseFormat'; +import duration from 'dayjs/esm/plugin/duration'; +import relativeTime from 'dayjs/esm/plugin/relativeTime'; + +// jhipster-needle-i18n-language-dayjs-imports - JHipster will import languages from dayjs here +import 'dayjs/esm/locale/pt'; +import 'dayjs/esm/locale/en'; + +// DAYJS CONFIGURATION +dayjs.extend(customParseFormat); +dayjs.extend(duration); +dayjs.extend(relativeTime); + +// Named export +export { dayjs }; \ No newline at end of file diff --git a/src/main/webapp/app/config/error.constants.ts b/src/main/webapp/app/config/error.constants.ts new file mode 100644 index 0000000..eff19a3 --- /dev/null +++ b/src/main/webapp/app/config/error.constants.ts @@ -0,0 +1,3 @@ +export const PROBLEM_BASE_URL = 'https://www.jhipster.tech/problem'; +export const EMAIL_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/email-already-used`; +export const LOGIN_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/login-already-used`; diff --git a/src/main/webapp/app/config/font-awesome-icons.ts b/src/main/webapp/app/config/font-awesome-icons.ts new file mode 100644 index 0000000..0e2ad07 --- /dev/null +++ b/src/main/webapp/app/config/font-awesome-icons.ts @@ -0,0 +1,109 @@ +import { + faArrowLeft, + faAsterisk, + faBan, + faBars, + faBell, + faBook, + faCalendarAlt, + faCheck, + faCircleQuestion, + faCloud, + faCogs, + faDatabase, + faEye, + faFlag, + faHeart, + faHome, + faList, + faLock, + faQuestion, + faPencilAlt, + faPlus, + faRoad, + faSave, + faSearch, + faSignOutAlt, + faSignInAlt, + faSitemap, + faSort, + faSortDown, + faSortUp, + faSync, + faTachometerAlt, + faTasks, + faThList, + faTimes, + faTrashAlt, + faUser, + faUserPlus, + faUsers, + faUsersCog, + faWrench, + faCaretDown, + faCaretUp, + faSquareMinus, + faSquare, + faSquareCheck, + faXmark, + faCircle, + faGear, + faFileExcel, + faFilePdf, + // jhipster-needle-add-icon-import +} from '@fortawesome/free-solid-svg-icons'; + +export const fontAwesomeIcons = [ + faArrowLeft, + faAsterisk, + faBan, + faBars, + faBell, + faBook, + faCalendarAlt, + faCheck, + faCircleQuestion, + faCloud, + faCogs, + faDatabase, + faEye, + faFlag, + faHeart, + faHome, + faList, + faLock, + faQuestion, + faPencilAlt, + faPlus, + faRoad, + faSave, + faSearch, + faSignOutAlt, + faSignInAlt, + faSitemap, + faSort, + faSortDown, + faSortUp, + faSync, + faTachometerAlt, + faTasks, + faThList, + faTimes, + faTrashAlt, + faUser, + faUserPlus, + faUsers, + faUsersCog, + faWrench, + faCaretDown, + faCaretUp, + faSquareMinus, + faSquare, + faSquareCheck, + faXmark, + faCircle, + faGear, + faFileExcel, + faFilePdf, + // jhipster-needle-add-icon-import +]; diff --git a/src/main/webapp/app/config/input.constants.ts b/src/main/webapp/app/config/input.constants.ts new file mode 100644 index 0000000..1e3978a --- /dev/null +++ b/src/main/webapp/app/config/input.constants.ts @@ -0,0 +1,2 @@ +export const DATE_FORMAT = 'YYYY-MM-DD'; +export const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm'; diff --git a/src/main/webapp/app/config/language.constants.ts b/src/main/webapp/app/config/language.constants.ts new file mode 100644 index 0000000..4d2c06b --- /dev/null +++ b/src/main/webapp/app/config/language.constants.ts @@ -0,0 +1,9 @@ +/* + Languages codes are ISO_639-1 codes, see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + They are written in English to avoid character encoding issues (not a perfect solution) +*/ +export const LANGUAGES: string[] = [ + 'pt-pt', + 'en', + // jhipster-needle-i18n-language-constant - JHipster will add/remove languages in this array +]; diff --git a/src/main/webapp/app/config/navigation.constants.ts b/src/main/webapp/app/config/navigation.constants.ts new file mode 100644 index 0000000..609160d --- /dev/null +++ b/src/main/webapp/app/config/navigation.constants.ts @@ -0,0 +1,5 @@ +export const ASC = 'asc'; +export const DESC = 'desc'; +export const SORT = 'sort'; +export const ITEM_DELETED_EVENT = 'deleted'; +export const DEFAULT_SORT_DATA = 'defaultSort'; diff --git a/src/main/webapp/app/config/pagination.constants.ts b/src/main/webapp/app/config/pagination.constants.ts new file mode 100644 index 0000000..6bee3ff --- /dev/null +++ b/src/main/webapp/app/config/pagination.constants.ts @@ -0,0 +1,3 @@ +export const TOTAL_COUNT_RESPONSE_HEADER = 'X-Total-Count'; +export const PAGE_HEADER = 'page'; +export const ITEMS_PER_PAGE = 20; diff --git a/src/main/webapp/app/config/translation.config.ts b/src/main/webapp/app/config/translation.config.ts new file mode 100644 index 0000000..b5ac129 --- /dev/null +++ b/src/main/webapp/app/config/translation.config.ts @@ -0,0 +1,20 @@ +import { HttpClient } from '@angular/common/http'; +import { MissingTranslationHandler, MissingTranslationHandlerParams, TranslateLoader } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + +export const translationNotFoundMessage = 'translation-not-found'; + +export class MissingTranslationHandlerImpl implements MissingTranslationHandler { + handle(params: MissingTranslationHandlerParams): string { + const key = params.key; + return `${translationNotFoundMessage}[${key}]`; + } +} + +export function translatePartialLoader(http: HttpClient): TranslateLoader { + return new TranslateHttpLoader(http, 'i18n/', `.json?_=${I18N_HASH}`); +} + +export function missingTranslationHandler(): MissingTranslationHandler { + return new MissingTranslationHandlerImpl(); +} diff --git a/src/main/webapp/app/config/uib-pagination.config.ts b/src/main/webapp/app/config/uib-pagination.config.ts new file mode 100644 index 0000000..8fc4fdc --- /dev/null +++ b/src/main/webapp/app/config/uib-pagination.config.ts @@ -0,0 +1,15 @@ +import { inject, Injectable } from '@angular/core'; +import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap'; + +import { ITEMS_PER_PAGE } from 'app/config/pagination.constants'; + +@Injectable({ providedIn: 'root' }) +export class PaginationConfig { + private config = inject(NgbPaginationConfig); + constructor() { + this.config.boundaryLinks = true; + this.config.maxSize = 5; + this.config.pageSize = ITEMS_PER_PAGE; + this.config.size = 'sm'; + } +} diff --git a/src/main/webapp/app/core/auth/account.model.ts b/src/main/webapp/app/core/auth/account.model.ts new file mode 100644 index 0000000..ddffc3c --- /dev/null +++ b/src/main/webapp/app/core/auth/account.model.ts @@ -0,0 +1,17 @@ +import { IOrganization } from 'app/entities/organization/organization.model'; +import {ISecurityGroup} from 'app/security/security-group/security-group.model' + +export class Account { + constructor( + public activated: boolean, + public authorities: string[], + public email: string, + public firstName: string | null, + public langKey: string, + public lastName: string | null, + public login: string, + public imageUrl: string | null, + public securityGroup: ISecurityGroup | null, + public parentOrganization: IOrganization | null, + ) {} +} diff --git a/src/main/webapp/app/core/auth/account.service.spec.ts b/src/main/webapp/app/core/auth/account.service.spec.ts new file mode 100644 index 0000000..25239d4 --- /dev/null +++ b/src/main/webapp/app/core/auth/account.service.spec.ts @@ -0,0 +1,247 @@ +jest.mock('app/core/auth/state-storage.service'); + +import { Router } from '@angular/router'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { Account } from 'app/core/auth/account.model'; +import { Authority } from 'app/config/authority.constants'; +import { StateStorageService } from 'app/core/auth/state-storage.service'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; + +import { AccountService } from './account.service'; + +function accountWithAuthorities(authorities: string[]): Account { + return { + activated: true, + authorities, + email: '', + firstName: '', + langKey: '', + lastName: '', + login: '', + imageUrl: '', + }; +} + +describe('Account Service', () => { + let service: AccountService; + let applicationConfigService: ApplicationConfigService; + let httpMock: HttpTestingController; + let mockStorageService: StateStorageService; + let mockRouter: Router; + let mockTranslateService: TranslateService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, TranslateModule.forRoot()], + providers: [StateStorageService], + }); + + service = TestBed.inject(AccountService); + applicationConfigService = TestBed.inject(ApplicationConfigService); + httpMock = TestBed.inject(HttpTestingController); + mockStorageService = TestBed.inject(StateStorageService); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigateByUrl').mockImplementation(() => Promise.resolve(true)); + + mockTranslateService = TestBed.inject(TranslateService); + jest.spyOn(mockTranslateService, 'use').mockImplementation(() => of('')); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('save', () => { + it('should call account saving endpoint with correct values', () => { + // GIVEN + const account = accountWithAuthorities([]); + + // WHEN + service.save(account).subscribe(); + const testRequest = httpMock.expectOne({ method: 'POST', url: applicationConfigService.getEndpointFor('api/account') }); + testRequest.flush({}); + + // THEN + expect(testRequest.request.body).toEqual(account); + }); + }); + + describe('authenticate', () => { + it('authenticationState should emit null if input is null', () => { + // GIVEN + let userIdentity: Account | null = accountWithAuthorities([]); + service.getAuthenticationState().subscribe(account => (userIdentity = account)); + + // WHEN + service.authenticate(null); + + // THEN + expect(userIdentity).toBeNull(); + expect(service.isAuthenticated()).toBe(false); + }); + + it('authenticationState should emit the same account as was in input parameter', () => { + // GIVEN + const expectedResult = accountWithAuthorities([]); + let userIdentity: Account | null = null; + service.getAuthenticationState().subscribe(account => (userIdentity = account)); + + // WHEN + service.authenticate(expectedResult); + + // THEN + expect(userIdentity).toEqual(expectedResult); + expect(service.isAuthenticated()).toBe(true); + }); + }); + + describe('identity', () => { + it('should call /account only once if last call have not returned', () => { + // When I call + service.identity().subscribe(); + // Once more + service.identity().subscribe(); + // Then there is only request + httpMock.expectOne({ method: 'GET' }); + }); + + it('should call /account only once if not logged out after first authentication and should call /account again if user has logged out', () => { + // Given the user is authenticated + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({}); + + // When I call + service.identity().subscribe(); + + // Then there is no second request + httpMock.expectNone({ method: 'GET' }); + + // When I log out + service.authenticate(null); + // and then call + service.identity().subscribe(); + + // Then there is a new request + httpMock.expectOne({ method: 'GET' }); + }); + + describe('should change the language on authentication if necessary', () => { + it('should change language if user has not changed language manually', () => { + // GIVEN + mockStorageService.getLocale = jest.fn(() => null); + + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({ ...accountWithAuthorities([]), langKey: 'accountLang' }); + + // THEN + expect(mockTranslateService.use).toHaveBeenCalledWith('accountLang'); + }); + + it('should not change language if user has changed language manually', () => { + // GIVEN + mockStorageService.getLocale = jest.fn(() => 'sessionLang'); + + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({ ...accountWithAuthorities([]), langKey: 'accountLang' }); + + // THEN + expect(mockTranslateService.use).not.toHaveBeenCalled(); + }); + }); + + describe('navigateToStoredUrl', () => { + it('should navigate to the previous stored url post successful authentication', () => { + // GIVEN + mockStorageService.getUrl = jest.fn(() => 'admin/users?page=0'); + + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({}); + + // THEN + expect(mockStorageService.getUrl).toHaveBeenCalledTimes(1); + expect(mockStorageService.clearUrl).toHaveBeenCalledTimes(1); + expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('admin/users?page=0'); + }); + + it('should not navigate to the previous stored url when authentication fails', () => { + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).error(new ErrorEvent('')); + + // THEN + expect(mockStorageService.getUrl).not.toHaveBeenCalled(); + expect(mockStorageService.clearUrl).not.toHaveBeenCalled(); + expect(mockRouter.navigateByUrl).not.toHaveBeenCalled(); + }); + + it('should not navigate to the previous stored url when no such url exists post successful authentication', () => { + // GIVEN + mockStorageService.getUrl = jest.fn(() => null); + + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({}); + + // THEN + expect(mockStorageService.getUrl).toHaveBeenCalledTimes(1); + expect(mockStorageService.clearUrl).not.toHaveBeenCalled(); + expect(mockRouter.navigateByUrl).not.toHaveBeenCalled(); + }); + }); + }); + + describe('hasAnyAuthority', () => { + describe('hasAnyAuthority string parameter', () => { + it('should return false if user is not logged', () => { + const hasAuthority = service.hasAnyAuthority(Authority.USER); + expect(hasAuthority).toBe(false); + }); + + it('should return false if user is logged and has not authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority(Authority.ADMIN); + + expect(hasAuthority).toBe(false); + }); + + it('should return true if user is logged and has authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority(Authority.USER); + + expect(hasAuthority).toBe(true); + }); + }); + + describe('hasAnyAuthority array parameter', () => { + it('should return false if user is not logged', () => { + const hasAuthority = service.hasAnyAuthority([Authority.USER]); + expect(hasAuthority).toBeFalsy(); + }); + + it('should return false if user is logged and has not authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority([Authority.ADMIN]); + + expect(hasAuthority).toBe(false); + }); + + it('should return true if user is logged and has authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority([Authority.USER, Authority.ADMIN]); + + expect(hasAuthority).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts new file mode 100644 index 0000000..522a3e9 --- /dev/null +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -0,0 +1,215 @@ +import { inject, Injectable, Signal, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { HttpClient } from '@angular/common/http'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, ReplaySubject, of } from 'rxjs'; +import { shareReplay, tap, catchError } from 'rxjs/operators'; + +import { StateStorageService } from 'app/core/auth/state-storage.service'; +import { Account } from 'app/core/auth/account.model'; +import { ApplicationConfigService } from '../config/application-config.service'; + +import { SecurityAction } from 'app/security/security-action.model'; +import { ISecurityGroupPermission } from 'app/security/security-group/security-group-permission.model'; +import { SecurityPermission } from 'app/security/security-permission.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; + +@Injectable({ providedIn: 'root' }) +export class AccountService { + private userIdentity = signal(null); + private authenticationState = new ReplaySubject(1); + private accountCache$?: Observable | null; + + private translateService = inject(TranslateService); + private http = inject(HttpClient); + private stateStorageService = inject(StateStorageService); + private router = inject(Router); + private applicationConfigService = inject(ApplicationConfigService); + + save(account: Account): Observable<{}> { + return this.http.post(this.applicationConfigService.getEndpointFor('api/account'), account); + } + + authenticate(identity: Account | null): void { + this.userIdentity.set(identity); + this.authenticationState.next(this.userIdentity()); + if (!identity) { + this.accountCache$ = null; + } + } + + trackCurrentAccount(): Signal { + return this.userIdentity.asReadonly(); + } + + hasAnyAuthority(authorities: string[] | string): boolean { + const userIdentity = this.userIdentity(); + if (!userIdentity) { + return false; + } + if (!Array.isArray(authorities)) { + authorities = [authorities]; + } + return userIdentity.authorities.some((authority: string) => authorities.includes(authority)); + } + + identity(force?: boolean): Observable { + if (!this.accountCache$ || force) { + this.accountCache$ = this.fetch().pipe( + tap((account: Account) => { + this.authenticate(account); + + // After retrieve the account info, the language will be changed to + // the user's preferred language configured in the account setting + // unless user have choosed other language in the current session + if (!this.stateStorageService.getLocale()) { + this.translateService.use(account.langKey); + } + + this.navigateToStoredUrl(); + }), + shareReplay(), + ); + } + return this.accountCache$.pipe(catchError(() => of(null))); + } + + getParentOrganization(): IOrganization | null { + const userIdentity = this.userIdentity(); + if (!userIdentity) { + return null; + } + + return userIdentity.parentOrganization; + } + + isAuthenticated(): boolean { + return this.userIdentity() !== null; + } + + getAuthenticationState(): Observable { + return this.authenticationState.asObservable(); + } + + saml2Endpoint(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('api/account/saml2-endpoint'), { responseType: 'text' as 'text' }); + } + + private fetch(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('api/account')); + } + + private navigateToStoredUrl(): void { + // previousState can be set in the authExpiredInterceptor and in the userRouteAccessService + // if login is successful, go to stored previousState and clear previousState + const previousUrl = this.stateStorageService.getUrl(); + if (previousUrl) { + this.stateStorageService.clearUrl(); + this.router.navigateByUrl(previousUrl); + } + } + + /* Security Permission evaluation */ + public findPermission(resourceCode: string): ISecurityGroupPermission | null { + const userIdentity = this.userIdentity(); + if (!userIdentity) { + return null; + } + + const permission = userIdentity.securityGroup?.securityGroupPermissions?.find( + (perm) => perm.securityResource?.code === resourceCode + ); + + return permission ?? null; + } + + getPermission(evalResource: string, evalAction: SecurityAction): SecurityPermission { + const userIdentity = this.userIdentity(); + let securityPermission: SecurityPermission = SecurityPermission.NONE; + + // User must be authenticated + if (!userIdentity) { + return securityPermission; + } + + // ROLE_ADMIN has permission to all + if (this.hasAnyAuthority('ROLE_ADMIN')) { + return SecurityPermission.ALL; + } + + // User must have a security group. Deny + if (!userIdentity.securityGroup) { + return SecurityPermission.NONE; + } + + const permission = this.findPermission(evalResource); + if (permission) { + switch (evalAction) { + case SecurityAction.CREATE: + securityPermission = permission.createPermission!; + break; + case SecurityAction.READ: + securityPermission = permission.readPermission!; + break; + case SecurityAction.UPDATE: + securityPermission = permission.updatePermission!; + break; + case SecurityAction.DELETE: + securityPermission = permission.deletePermission!; + break; + } + } + + return securityPermission; + } + + evalPermission(evalResource: string, evalAction: SecurityAction): boolean { + const userIdentity = this.userIdentity(); + + // User must be authenticated + if (!userIdentity) { + return false; + } + + // ROLE_ADMIN has permission to all + if (this.hasAnyAuthority('ROLE_ADMIN')) { + return true; + } + + // User must have a security group + if (!userIdentity.securityGroup) { + return false; + } + + const evalResourceCode = evalResource; + // Any of this will give user permission. + // In case of SecurityPermission.HIERARCHY, it needs extra functional evaluation from .component.ts + const evalPermissions = [SecurityPermission.ALL, SecurityPermission.HIERARCHY]; + + const permission = this.findPermission(evalResourceCode); + if (permission) { + let actionPermission: SecurityPermission = SecurityPermission.NONE; + switch (evalAction) { + case SecurityAction.CREATE: + actionPermission = permission.createPermission!; + break; + case SecurityAction.READ: + actionPermission = permission.readPermission!; + break; + case SecurityAction.UPDATE: + actionPermission = permission.updatePermission!; + break; + case SecurityAction.DELETE: + actionPermission = permission.deletePermission!; + break; + default: + console.log('Unknown action'); + } + + return evalPermissions.includes(actionPermission); + } + + return false; + } + +} diff --git a/src/main/webapp/app/core/auth/auth-session.service.ts b/src/main/webapp/app/core/auth/auth-session.service.ts new file mode 100644 index 0000000..973b214 --- /dev/null +++ b/src/main/webapp/app/core/auth/auth-session.service.ts @@ -0,0 +1,35 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { Login } from 'app/login/login.model'; +import { ApplicationConfigService } from '../config/application-config.service'; + +@Injectable({ providedIn: 'root' }) +export class AuthServerProvider { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + login(credentials: Login): Observable<{}> { + const data = + `username=${encodeURIComponent(credentials.username)}` + + `&password=${encodeURIComponent(credentials.password)}` + + `&remember-me=${credentials.rememberMe ? 'true' : 'false'}` + + '&submit=Login'; + + const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'); + + return this.http.post(this.applicationConfigService.getEndpointFor('api/authentication'), data, { headers }); + } + + logout(): Observable { + // logout from the server + return this.http.post(this.applicationConfigService.getEndpointFor('api/logout'), {}).pipe( + map(() => { + // to get a new csrf token call the api + this.http.get(this.applicationConfigService.getEndpointFor('api/account')).subscribe(); + }), + ); + } +} diff --git a/src/main/webapp/app/core/auth/state-storage.service.ts b/src/main/webapp/app/core/auth/state-storage.service.ts new file mode 100644 index 0000000..14eec1b --- /dev/null +++ b/src/main/webapp/app/core/auth/state-storage.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class StateStorageService { + private previousUrlKey = 'previousUrl'; + private authenticationKey = 'jhi-authenticationToken'; + private localeKey = 'locale'; + + storeUrl(url: string): void { + sessionStorage.setItem(this.previousUrlKey, JSON.stringify(url)); + } + + getUrl(): string | null { + const previousUrl = sessionStorage.getItem(this.previousUrlKey); + return previousUrl ? (JSON.parse(previousUrl) as string | null) : previousUrl; + } + + clearUrl(): void { + sessionStorage.removeItem(this.previousUrlKey); + } + + storeAuthenticationToken(authenticationToken: string, rememberMe: boolean): void { + authenticationToken = JSON.stringify(authenticationToken); + this.clearAuthenticationToken(); + if (rememberMe) { + localStorage.setItem(this.authenticationKey, authenticationToken); + } else { + sessionStorage.setItem(this.authenticationKey, authenticationToken); + } + } + + getAuthenticationToken(): string | null { + const authenticationToken = localStorage.getItem(this.authenticationKey) ?? sessionStorage.getItem(this.authenticationKey); + return authenticationToken ? (JSON.parse(authenticationToken) as string | null) : authenticationToken; + } + + clearAuthenticationToken(): void { + sessionStorage.removeItem(this.authenticationKey); + localStorage.removeItem(this.authenticationKey); + } + + storeLocale(locale: string): void { + sessionStorage.setItem(this.localeKey, locale); + } + + getLocale(): string | null { + return sessionStorage.getItem(this.localeKey); + } + + clearLocale(): void { + sessionStorage.removeItem(this.localeKey); + } +} diff --git a/src/main/webapp/app/core/auth/user-route-access.service.ts b/src/main/webapp/app/core/auth/user-route-access.service.ts new file mode 100644 index 0000000..58bbbae --- /dev/null +++ b/src/main/webapp/app/core/auth/user-route-access.service.ts @@ -0,0 +1,84 @@ +import { Injector, inject, isDevMode } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; +import { map } from 'rxjs/operators'; + +import { AccountService } from 'app/core/auth/account.service'; +import { StateStorageService } from './state-storage.service'; +import { SecurityAction} from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; + +/** + * Used to secure a router path. + * Usage in *.routes.ts: + const dashboardComponentRoute: Routes = [ + { + path: ':id/view', + component: DashboardComponentDetailComponent, + resolve: { + dashboardComponent: DashboardComponentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + resource: Resources.DASHBOARD_COMPONENT, + service: DashboardComponentService + } + }, + (...) + * + * Can receive data[] args: + * - action - one of SecurityAction action + * - resource - the SecurityResource to evaluate permission + * - service - the EntityService + * + * @param next + * @param state + * @returns + */ +export const UserRouteAccessService: CanActivateFn = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const injector = inject(Injector); + const accountService = inject(AccountService); + const router = inject(Router); + const stateStorageService = inject(StateStorageService); + + return accountService.identity().pipe( + map(account => { + if (account) { + let auth = true; // Defaults to TRUE. ROLES or SecurityGroup must say otherwise + + // Check for ROLES (if any) + const authorities = next.data['authorities']; + + if (authorities && authorities.length > 0) { + auth = accountService.hasAnyAuthority(authorities); + if (isDevMode() && !auth) { + console.error('User does not have any of the required authorities:', authorities); + } + } + + // Check for permission in SecurityGroup, if it didn't failed before + if (auth) { + const actionToken: SecurityAction | null = next.data['action']; + const resourceToken: Resources | null = next.data['resource']; + const serviceToken = next.data['service']; + + if (actionToken && serviceToken && resourceToken) { + // Make security evaluation with EntityService + auth = accountService.evalPermission(resourceToken, actionToken); + if (isDevMode() && !auth) { + console.error('User does not have permisison to (action/resource) :', actionToken, resourceToken); + } + } + } + + if (!auth) router.navigate(['accessdenied']); + return auth; + } + + stateStorageService.storeUrl(state.url); + router.navigate(['/login']); + return false; + }), + ); + +}; diff --git a/src/main/webapp/app/core/config/application-config.service.spec.ts b/src/main/webapp/app/core/config/application-config.service.spec.ts new file mode 100644 index 0000000..4451c9b --- /dev/null +++ b/src/main/webapp/app/core/config/application-config.service.spec.ts @@ -0,0 +1,40 @@ +import { TestBed } from '@angular/core/testing'; + +import { ApplicationConfigService } from './application-config.service'; + +describe('ApplicationConfigService', () => { + let service: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ApplicationConfigService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('without prefix', () => { + it('should return correctly', () => { + expect(service.getEndpointFor('api')).toEqual('api'); + }); + + it('should return correctly when passing microservice', () => { + expect(service.getEndpointFor('api', 'microservice')).toEqual('services/microservice/api'); + }); + }); + + describe('with prefix', () => { + beforeEach(() => { + service.setEndpointPrefix('prefix/'); + }); + + it('should return correctly', () => { + expect(service.getEndpointFor('api')).toEqual('prefix/api'); + }); + + it('should return correctly when passing microservice', () => { + expect(service.getEndpointFor('api', 'microservice')).toEqual('prefix/services/microservice/api'); + }); + }); +}); diff --git a/src/main/webapp/app/core/config/application-config.service.ts b/src/main/webapp/app/core/config/application-config.service.ts new file mode 100644 index 0000000..0102e5f --- /dev/null +++ b/src/main/webapp/app/core/config/application-config.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationConfigService { + private endpointPrefix = ''; + private microfrontend = false; + + setEndpointPrefix(endpointPrefix: string): void { + this.endpointPrefix = endpointPrefix; + } + + setMicrofrontend(microfrontend = true): void { + this.microfrontend = microfrontend; + } + + isMicrofrontend(): boolean { + return this.microfrontend; + } + + getEndpointFor(api: string, microservice?: string): string { + if (microservice) { + return `${this.endpointPrefix}services/${microservice}/${api}`; + } + return `${this.endpointPrefix}${api}`; + } +} diff --git a/src/main/webapp/app/core/interceptor/auth-expired.interceptor.ts b/src/main/webapp/app/core/interceptor/auth-expired.interceptor.ts new file mode 100644 index 0000000..e34b8a5 --- /dev/null +++ b/src/main/webapp/app/core/interceptor/auth-expired.interceptor.ts @@ -0,0 +1,33 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { Router } from '@angular/router'; + +import { LoginService } from 'app/login/login.service'; +import { StateStorageService } from 'app/core/auth/state-storage.service'; + +@Injectable() +export class AuthExpiredInterceptor implements HttpInterceptor { + private loginService = inject(LoginService); + private stateStorageService = inject(StateStorageService); + private router = inject(Router); + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + tap({ + error: (err: HttpErrorResponse) => { + if (err.status === 401 && err.url && !err.url.includes('api/account')) { + if (err.url.includes(this.loginService.logoutUrl())) { + this.loginService.logoutInClient(); + return; + } + this.stateStorageService.storeUrl(this.router.routerState.snapshot.url); + this.loginService.logout(); + this.router.navigate(['/login']); + } + }, + }), + ); + } +} diff --git a/src/main/webapp/app/core/interceptor/error-handler.interceptor.ts b/src/main/webapp/app/core/interceptor/error-handler.interceptor.ts new file mode 100644 index 0000000..acf09e5 --- /dev/null +++ b/src/main/webapp/app/core/interceptor/error-handler.interceptor.ts @@ -0,0 +1,23 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpErrorResponse, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; + +@Injectable() +export class ErrorHandlerInterceptor implements HttpInterceptor { + private eventManager = inject(EventManager); + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + tap({ + error: (err: HttpErrorResponse) => { + if (!(err.status === 401 && (err.message === '' || err.url?.includes('api/account')))) { + this.eventManager.broadcast(new EventWithContent('resilientApp.httpError', err)); + } + }, + }), + ); + } +} diff --git a/src/main/webapp/app/core/interceptor/index.ts b/src/main/webapp/app/core/interceptor/index.ts new file mode 100644 index 0000000..a4f96d0 --- /dev/null +++ b/src/main/webapp/app/core/interceptor/index.ts @@ -0,0 +1,23 @@ +import { HTTP_INTERCEPTORS } from '@angular/common/http'; + +import { AuthExpiredInterceptor } from 'app/core/interceptor/auth-expired.interceptor'; +import { ErrorHandlerInterceptor } from 'app/core/interceptor/error-handler.interceptor'; +import { NotificationInterceptor } from 'app/core/interceptor/notification.interceptor'; + +export const httpInterceptorProviders = [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthExpiredInterceptor, + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorHandlerInterceptor, + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: NotificationInterceptor, + multi: true, + }, +]; diff --git a/src/main/webapp/app/core/interceptor/notification.interceptor.ts b/src/main/webapp/app/core/interceptor/notification.interceptor.ts new file mode 100644 index 0000000..b1ae5c3 --- /dev/null +++ b/src/main/webapp/app/core/interceptor/notification.interceptor.ts @@ -0,0 +1,38 @@ +import { HttpInterceptor, HttpRequest, HttpResponse, HttpHandler, HttpEvent } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { AlertService } from 'app/core/util/alert.service'; + +@Injectable() +export class NotificationInterceptor implements HttpInterceptor { + private alertService = inject(AlertService); + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + tap((event: HttpEvent) => { + if (event instanceof HttpResponse) { + let alert: string | null = null; + let alertParams: string | null = null; + + for (const headerKey of event.headers.keys()) { + if (headerKey.toLowerCase().endsWith('app-alert')) { + alert = event.headers.get(headerKey); + } else if (headerKey.toLowerCase().endsWith('app-params')) { + alertParams = decodeURIComponent(event.headers.get(headerKey)!.replace(/\+/g, ' ')); + } + } + + if (alert) { + this.alertService.addAlert({ + type: 'success', + translationKey: alert, + translationParams: { param: alertParams }, + }); + } + } + }), + ); + } +} diff --git a/src/main/webapp/app/core/request/request-util.ts b/src/main/webapp/app/core/request/request-util.ts new file mode 100644 index 0000000..207dfb6 --- /dev/null +++ b/src/main/webapp/app/core/request/request-util.ts @@ -0,0 +1,17 @@ +import { HttpParams } from '@angular/common/http'; + +export const createRequestOption = (req?: any): HttpParams => { + let options: HttpParams = new HttpParams(); + + if (req) { + Object.entries(req).forEach(([key, val]) => { + if (val !== undefined && val !== null) { + for (const value of [].concat(req[key]).filter(v => v !== '')) { + options = options.append(key, value); + } + } + }); + } + + return options; +}; diff --git a/src/main/webapp/app/core/request/request.model.ts b/src/main/webapp/app/core/request/request.model.ts new file mode 100644 index 0000000..5de2b69 --- /dev/null +++ b/src/main/webapp/app/core/request/request.model.ts @@ -0,0 +1,11 @@ +export interface Pagination { + page: number; + size: number; + sort: string[]; +} + +export interface Search { + query: string; +} + +export interface SearchWithPagination extends Search, Pagination {} diff --git a/src/main/webapp/app/core/util/alert.service.spec.ts b/src/main/webapp/app/core/util/alert.service.spec.ts new file mode 100644 index 0000000..05a8c57 --- /dev/null +++ b/src/main/webapp/app/core/util/alert.service.spec.ts @@ -0,0 +1,285 @@ +import { inject, TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService, MissingTranslationHandler } from '@ngx-translate/core'; +import { missingTranslationHandler } from '../../config/translation.config'; + +import { Alert, AlertService } from './alert.service'; + +describe('Alert service test', () => { + describe('Alert Service Test', () => { + let extAlerts: Alert[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + missingTranslationHandler: { + provide: MissingTranslationHandler, + useFactory: missingTranslationHandler, + }, + }), + ], + }); + const translateService = TestBed.inject(TranslateService); + translateService.setDefaultLang('en'); + jest.useFakeTimers(); + extAlerts = []; + }); + + it('should produce a proper alert object and fetch it', inject([AlertService], (service: AlertService) => { + expect( + service.addAlert({ + type: 'success', + message: 'Hello Jhipster', + timeout: 3000, + toast: true, + position: 'top left', + }), + ).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert), + ); + + expect(service.get().length).toBe(1); + expect(service.get()[0]).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert), + ); + })); + + it('should produce a proper alert object and add it to external alert objects array', inject( + [AlertService], + (service: AlertService) => { + expect( + service.addAlert( + { + type: 'success', + message: 'Hello Jhipster', + timeout: 3000, + toast: true, + position: 'top left', + }, + extAlerts, + ), + ).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert), + ); + + expect(extAlerts.length).toBe(1); + expect(extAlerts[0]).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert), + ); + }, + )); + + it('should produce an alert object with correct id', inject([AlertService], (service: AlertService) => { + service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + expect(service.addAlert({ type: 'success', message: 'Hello Jhipster success' })).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 1, + } as Alert), + ); + + expect(service.get().length).toBe(2); + expect(service.get()[1]).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 1, + } as Alert), + ); + })); + + it('should close an alert correctly', inject([AlertService], (service: AlertService) => { + const alert0 = service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + const alert1 = service.addAlert({ type: 'info', message: 'Hello Jhipster info 2' }); + const alert2 = service.addAlert({ type: 'success', message: 'Hello Jhipster success' }); + expect(alert2).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 2, + } as Alert), + ); + + expect(service.get().length).toBe(3); + alert1.close?.(service.get()); + expect(service.get().length).toBe(2); + expect(service.get()[1]).not.toEqual( + expect.objectContaining({ + type: 'info', + message: 'Hello Jhipster info 2', + id: 1, + } as Alert), + ); + alert2.close?.(service.get()); + expect(service.get().length).toBe(1); + expect(service.get()[0]).not.toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 2, + } as Alert), + ); + alert0.close?.(service.get()); + expect(service.get().length).toBe(0); + })); + + it('should close an alert on timeout correctly', inject([AlertService], (service: AlertService) => { + service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + + expect(service.get().length).toBe(1); + + jest.advanceTimersByTime(6000); + + expect(service.get().length).toBe(0); + })); + + it('should clear alerts', inject([AlertService], (service: AlertService) => { + service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + service.addAlert({ type: 'danger', message: 'Hello Jhipster info' }); + service.addAlert({ type: 'success', message: 'Hello Jhipster info' }); + expect(service.get().length).toBe(3); + service.clear(); + expect(service.get().length).toBe(0); + })); + + it('should produce a scoped alert', inject([AlertService], (service: AlertService) => { + expect( + service.addAlert( + { + type: 'success', + message: 'Hello Jhipster', + timeout: 3000, + toast: true, + position: 'top left', + }, + [], + ), + ).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert), + ); + + expect(service.get().length).toBe(0); + })); + + it('should produce a success message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'success', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + } as Alert), + ); + })); + + it('should produce a success message with custom position', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'success', message: 'Hello Jhipster', position: 'bottom left' })).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + position: 'bottom left', + } as Alert), + ); + })); + + it('should produce a error message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'danger', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'danger', + message: 'Hello Jhipster', + } as Alert), + ); + })); + + it('should produce a warning message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'warning', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'warning', + message: 'Hello Jhipster', + } as Alert), + ); + })); + + it('should produce a info message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'info', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'info', + message: 'Hello Jhipster', + } as Alert), + ); + })); + + it('should produce a info message with translated message if key exists', inject( + [AlertService, TranslateService], + (service: AlertService, translateService: TranslateService) => { + translateService.setTranslation('en', { + 'hello.jhipster': 'Translated message', + }); + expect(service.addAlert({ type: 'info', message: 'Hello Jhipster', translationKey: 'hello.jhipster' })).toEqual( + expect.objectContaining({ + type: 'info', + message: 'Translated message', + } as Alert), + ); + }, + )); + + it('should produce a info message with provided message if key does not exists', inject( + [AlertService, TranslateService], + (service: AlertService) => { + expect(service.addAlert({ type: 'info', message: 'Hello Jhipster', translationKey: 'hello.jhipster' })).toEqual( + expect.objectContaining({ + type: 'info', + message: 'Hello Jhipster', + } as Alert), + ); + }, + )); + + it('should produce a info message with provided key if translation key does not exist in translations and message is not provided', inject( + [AlertService, TranslateService], + (service: AlertService) => { + expect(service.addAlert({ type: 'info', translationKey: 'hello.jhipster' })).toEqual( + expect.objectContaining({ + type: 'info', + message: 'hello.jhipster', + } as Alert), + ); + }, + )); + }); +}); diff --git a/src/main/webapp/app/core/util/alert.service.ts b/src/main/webapp/app/core/util/alert.service.ts new file mode 100644 index 0000000..4e4018e --- /dev/null +++ b/src/main/webapp/app/core/util/alert.service.ts @@ -0,0 +1,90 @@ +import { inject, Injectable, SecurityContext } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { TranslateService } from '@ngx-translate/core'; + +import { translationNotFoundMessage } from 'app/config/translation.config'; + +export type AlertType = 'success' | 'danger' | 'warning' | 'info'; + +export interface Alert { + id?: number; + type: AlertType; + message?: string; + translationKey?: string; + translationParams?: { [key: string]: unknown }; + timeout?: number; + toast?: boolean; + position?: string; + close?: (alerts: Alert[]) => void; +} + +@Injectable({ + providedIn: 'root', +}) +export class AlertService { + timeout = 5000; + toast = false; + position = 'top right'; + + // unique id for each alert. Starts from 0. + private alertId = 0; + private alerts: Alert[] = []; + + private sanitizer = inject(DomSanitizer); + private translateService = inject(TranslateService); + + clear(): void { + this.alerts = []; + } + + get(): Alert[] { + return this.alerts; + } + + /** + * Adds alert to alerts array and returns added alert. + * @param alert Alert to add. If `timeout`, `toast` or `position` is missing then applying default value. + * If `translateKey` is available then it's translation else `message` is used for showing. + * @param extAlerts If missing then adding `alert` to `AlertService` internal array and alerts can be retrieved by `get()`. + * Else adding `alert` to `extAlerts`. + * @returns Added alert + */ + addAlert(alert: Alert, extAlerts?: Alert[]): Alert { + alert.id = this.alertId++; + + if (alert.translationKey) { + const translatedMessage = this.translateService.instant(alert.translationKey, alert.translationParams); + // if translation key exists + if (translatedMessage !== `${translationNotFoundMessage}[${alert.translationKey}]`) { + alert.message = translatedMessage; + } else if (!alert.message) { + alert.message = alert.translationKey; + } + } + + alert.message = this.sanitizer.sanitize(SecurityContext.HTML, alert.message ?? '') ?? ''; + alert.timeout = alert.timeout ?? this.timeout; + alert.toast = alert.toast ?? this.toast; + alert.position = alert.position ?? this.position; + alert.close = (alertsArray: Alert[]) => this.closeAlert(alert.id!, alertsArray); + + (extAlerts ?? this.alerts).push(alert); + + if (alert.timeout > 0) { + setTimeout(() => { + this.closeAlert(alert.id!, extAlerts ?? this.alerts); + }, alert.timeout); + } + + return alert; + } + + private closeAlert(alertId: number, extAlerts?: Alert[]): void { + const alerts = extAlerts ?? this.alerts; + const alertIndex = alerts.map(alert => alert.id).indexOf(alertId); + // if found alert then remove + if (alertIndex >= 0) { + alerts.splice(alertIndex, 1); + } + } +} diff --git a/src/main/webapp/app/core/util/data-util.service.spec.ts b/src/main/webapp/app/core/util/data-util.service.spec.ts new file mode 100644 index 0000000..fccbcc6 --- /dev/null +++ b/src/main/webapp/app/core/util/data-util.service.spec.ts @@ -0,0 +1,34 @@ +import { TestBed } from '@angular/core/testing'; + +import { DataUtils } from './data-util.service'; + +describe('Data Utils Service Test', () => { + let service: DataUtils; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DataUtils], + }); + service = TestBed.inject(DataUtils); + }); + + describe('byteSize', () => { + it('should return the bytesize of the text', () => { + expect(service.byteSize('Hello JHipster')).toBe(`10.5 bytes`); + }); + }); + + describe('openFile', () => { + it('should open the file in the new window', () => { + const newWindow = { ...window }; + newWindow.document.write = jest.fn(); + window.open = jest.fn(() => newWindow); + window.URL.createObjectURL = jest.fn(); + // 'JHipster' in base64 is 'SkhpcHN0ZXI=' + const data = 'SkhpcHN0ZXI='; + const contentType = 'text/plain'; + service.openFile(data, contentType); + expect(window.open).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/main/webapp/app/core/util/data-util.service.ts b/src/main/webapp/app/core/util/data-util.service.ts new file mode 100644 index 0000000..bb88202 --- /dev/null +++ b/src/main/webapp/app/core/util/data-util.service.ts @@ -0,0 +1,130 @@ +import { Buffer } from 'buffer'; +import { Injectable } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Observable, Observer } from 'rxjs'; + +export type FileLoadErrorType = 'not.image' | 'could.not.extract'; + +export interface FileLoadError { + message: string; + key: FileLoadErrorType; + params?: any; +} + +/** + * An utility service for data. + */ +@Injectable({ + providedIn: 'root', +}) +export class DataUtils { + /** + * Method to find the byte size of the string provides + */ + byteSize(base64String: string): string { + return this.formatAsBytes(this.size(base64String)); + } + + /** + * Method to open file + */ + openFile(data: string, contentType: string | null | undefined): void { + contentType = contentType ?? ''; + + const byteCharacters = Buffer.from(data, 'base64').toString('binary'); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { + type: contentType, + }); + const fileURL = window.URL.createObjectURL(blob); + const win = window.open(fileURL); + win!.onload = function () { + URL.revokeObjectURL(fileURL); + }; + } + + /** + * Sets the base 64 data & file type of the 1st file on the event (event.target.files[0]) in the passed entity object + * and returns an observable. + * + * @param event the object containing the file (at event.target.files[0]) + * @param editForm the form group where the input field is located + * @param field the field name to set the file's 'base 64 data' on + * @param isImage boolean representing if the file represented by the event is an image + * @returns an observable that loads file to form field and completes if successful + * or returns error as FileLoadError on failure + */ + loadFileToForm(event: Event, editForm: FormGroup, field: string, isImage: boolean): Observable { + return new Observable((observer: Observer) => { + const eventTarget: HTMLInputElement | null = event.target as HTMLInputElement | null; + if (eventTarget?.files?.[0]) { + const file: File = eventTarget.files[0]; + if (isImage && !file.type.startsWith('image/')) { + const error: FileLoadError = { + message: `File was expected to be an image but was found to be '${file.type}'`, + key: 'not.image', + params: { fileType: file.type }, + }; + observer.error(error); + } else { + const fieldContentType: string = field + 'ContentType'; + this.toBase64(file, (base64Data: string) => { + editForm.patchValue({ + [field]: base64Data, + [fieldContentType]: file.type, + }); + observer.next(); + observer.complete(); + }); + } + } else { + const error: FileLoadError = { + message: 'Could not extract file', + key: 'could.not.extract', + params: { event }, + }; + observer.error(error); + } + }); + } + + /** + * Method to convert the file to base64 + */ + private toBase64(file: File, callback: (base64Data: string) => void): void { + const fileReader: FileReader = new FileReader(); + fileReader.onload = (e: ProgressEvent) => { + if (typeof e.target?.result === 'string') { + const base64Data: string = e.target.result.substring(e.target.result.indexOf('base64,') + 'base64,'.length); + callback(base64Data); + } + }; + fileReader.readAsDataURL(file); + } + + private endsWith(suffix: string, str: string): boolean { + return str.includes(suffix, str.length - suffix.length); + } + + private paddingSize(value: string): number { + if (this.endsWith('==', value)) { + return 2; + } + if (this.endsWith('=', value)) { + return 1; + } + return 0; + } + + private size(value: string): number { + return (value.length / 4) * 3 - this.paddingSize(value); + } + + private formatAsBytes(size: number): string { + return size.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + ' bytes'; // NOSONAR + } +} diff --git a/src/main/webapp/app/core/util/event-manager.service.spec.ts b/src/main/webapp/app/core/util/event-manager.service.spec.ts new file mode 100644 index 0000000..1d98ba3 --- /dev/null +++ b/src/main/webapp/app/core/util/event-manager.service.spec.ts @@ -0,0 +1,84 @@ +import { inject, TestBed } from '@angular/core/testing'; + +import { EventManager, EventWithContent } from './event-manager.service'; + +describe('Event Manager tests', () => { + describe('EventWithContent', () => { + it('should create correctly EventWithContent', () => { + // WHEN + const eventWithContent = new EventWithContent('name', 'content'); + + // THEN + expect(eventWithContent).toEqual({ name: 'name', content: 'content' }); + }); + }); + + describe('EventManager', () => { + let receivedEvent: EventWithContent | string | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [EventManager], + }); + receivedEvent = null; + }); + + it('should not fail when nosubscriber and broadcasting', inject([EventManager], (eventManager: EventManager) => { + expect(eventManager.observer).toBeUndefined(); + eventManager.broadcast({ name: 'modifier', content: 'modified something' }); + })); + + it('should create an observable and callback when broadcasted EventWithContent', inject( + [EventManager], + (eventManager: EventManager) => { + // GIVEN + eventManager.subscribe('modifier', (event: EventWithContent | string) => (receivedEvent = event)); + + // WHEN + eventManager.broadcast({ name: 'unrelatedModifier', content: 'unrelated modification' }); + // THEN + expect(receivedEvent).toBeNull(); + + // WHEN + eventManager.broadcast({ name: 'modifier', content: 'modified something' }); + // THEN + expect(receivedEvent).toEqual({ name: 'modifier', content: 'modified something' }); + }, + )); + + it('should create an observable and callback when broadcasted string', inject([EventManager], (eventManager: EventManager) => { + // GIVEN + eventManager.subscribe('modifier', (event: EventWithContent | string) => (receivedEvent = event)); + + // WHEN + eventManager.broadcast('unrelatedModifier'); + // THEN + expect(receivedEvent).toBeNull(); + + // WHEN + eventManager.broadcast('modifier'); + // THEN + expect(receivedEvent).toEqual('modifier'); + })); + + it('should subscribe to multiple events', inject([EventManager], (eventManager: EventManager) => { + // GIVEN + eventManager.subscribe(['modifier', 'modifier2'], (event: EventWithContent | string) => (receivedEvent = event)); + + // WHEN + eventManager.broadcast('unrelatedModifier'); + // THEN + expect(receivedEvent).toBeNull(); + + // WHEN + eventManager.broadcast({ name: 'modifier', content: 'modified something' }); + // THEN + expect(receivedEvent).toEqual({ name: 'modifier', content: 'modified something' }); + + // WHEN + eventManager.broadcast('modifier2'); + // THEN + expect(receivedEvent).toEqual('modifier2'); + })); + }); +}); diff --git a/src/main/webapp/app/core/util/event-manager.service.ts b/src/main/webapp/app/core/util/event-manager.service.ts new file mode 100644 index 0000000..a73d212 --- /dev/null +++ b/src/main/webapp/app/core/util/event-manager.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; +import { Observable, Observer, Subscription } from 'rxjs'; +import { filter, share } from 'rxjs/operators'; + +export class EventWithContent { + constructor( + public name: string, + public content: T, + ) {} +} + +/** + * An utility class to manage RX events + */ +@Injectable({ + providedIn: 'root', +}) +export class EventManager { + observable: Observable | string>; + observer?: Observer | string>; + + constructor() { + this.observable = new Observable((observer: Observer | string>) => { + this.observer = observer; + }).pipe(share()); + } + + /** + * Method to broadcast the event to observer + */ + broadcast(event: EventWithContent | string): void { + if (this.observer) { + this.observer.next(event); + } + } + + /** + * Method to subscribe to an event with callback + * @param eventNames Single event name or array of event names to what subscribe + * @param callback Callback to run when the event occurs + */ + subscribe(eventNames: string | string[], callback: (event: EventWithContent | string) => void): Subscription { + if (typeof eventNames === 'string') { + eventNames = [eventNames]; + } + return this.observable + .pipe( + filter((event: EventWithContent | string) => { + for (const eventName of eventNames) { + if ((typeof event === 'string' && event === eventName) || (typeof event !== 'string' && event.name === eventName)) { + return true; + } + } + return false; + }), + ) + .subscribe(callback); + } + + /** + * Method to unsubscribe the subscription + */ + destroy(subscriber: Subscription): void { + subscriber.unsubscribe(); + } +} diff --git a/src/main/webapp/app/core/util/operators.spec.ts b/src/main/webapp/app/core/util/operators.spec.ts new file mode 100644 index 0000000..429647c --- /dev/null +++ b/src/main/webapp/app/core/util/operators.spec.ts @@ -0,0 +1,18 @@ +import { filterNaN, isPresent } from './operators'; + +describe('Operators Test', () => { + describe('isPresent', () => { + it('should remove null and undefined values', () => { + expect([1, null, undefined].filter(isPresent)).toEqual([1]); + }); + }); + + describe('filterNaN', () => { + it('should return 0 for NaN', () => { + expect(filterNaN(NaN)).toBe(0); + }); + it('should return number for a number', () => { + expect(filterNaN(12345)).toBe(12345); + }); + }); +}); diff --git a/src/main/webapp/app/core/util/operators.ts b/src/main/webapp/app/core/util/operators.ts new file mode 100644 index 0000000..c224592 --- /dev/null +++ b/src/main/webapp/app/core/util/operators.ts @@ -0,0 +1,9 @@ +/* + * Function used to workaround https://github.com/microsoft/TypeScript/issues/16069 + * es2019 alternative `const filteredArr = myArr.flatMap((x) => x ? x : []);` + */ +export function isPresent(t: T | undefined | null | void): t is T { + return t !== undefined && t !== null; +} + +export const filterNaN = (input: number): number => (isNaN(input) ? 0 : input); diff --git a/src/main/webapp/app/core/util/parse-links.service.spec.ts b/src/main/webapp/app/core/util/parse-links.service.spec.ts new file mode 100644 index 0000000..331bdf8 --- /dev/null +++ b/src/main/webapp/app/core/util/parse-links.service.spec.ts @@ -0,0 +1,66 @@ +import { inject, TestBed } from '@angular/core/testing'; + +import { ParseLinks } from './parse-links.service'; + +describe('Parse links service test', () => { + describe('parse', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ParseLinks], + }); + }); + + it('should throw an error when passed an empty string', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parse(''); + }).toThrow(new Error('input must not be of zero length')); + })); + + it('should throw an error when passed without comma', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parse('test'); + }).toThrow(new Error('section could not be split on ";"')); + })); + + it('should throw an error when passed without semicolon', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parse('test,test2'); + }).toThrow(new Error('section could not be split on ";"')); + })); + + it('should return links when headers are passed', inject([ParseLinks], (service: ParseLinks) => { + const links = { last: 0, first: 0 }; + expect(service.parse(' ; rel="last",; rel="first"')).toEqual(links); + })); + }); + describe('parseAll', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ParseLinks], + }); + }); + + it('should throw an error when passed an empty string', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parseAll(''); + }).toThrow(new Error('input must not be of zero length')); + })); + + it('should throw an error when passed without comma', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parseAll('test'); + }).toThrow(new Error('section could not be split on ";"')); + })); + + it('should throw an error when passed without semicolon', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parseAll('test,test2'); + }).toThrow(new Error('section could not be split on ";"')); + })); + + it('should return links when headers are passed', inject([ParseLinks], (service: ParseLinks) => { + const links = { last: { page: '0', size: '20' }, first: { page: '0', size: '20' } }; + expect(service.parseAll(' ; rel="last",; rel="first"')).toEqual(links); + })); + }); +}); diff --git a/src/main/webapp/app/core/util/parse-links.service.ts b/src/main/webapp/app/core/util/parse-links.service.ts new file mode 100644 index 0000000..9aa4530 --- /dev/null +++ b/src/main/webapp/app/core/util/parse-links.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; + +/** + * An utility service for link parsing. + */ +@Injectable({ + providedIn: 'root', +}) +export class ParseLinks { + /** + * Method to parse the links + */ + parseAll(header: string): { [key: string]: { [key: string]: string | undefined } | undefined } { + if (header.length === 0) { + throw new Error('input must not be of zero length'); + } + + // Split parts by comma + const parts: string[] = header.split(','); + + // Parse each part into a named link + return Object.fromEntries( + parts.map(p => { + const section: string[] = p.split(';'); + + if (section.length !== 2) { + throw new Error('section could not be split on ";"'); + } + + const url: string = section[0].replace(/<(.*)>/, '$1').trim(); // NOSONAR + const queryString: { [key: string]: string } = {}; + + url.replace(/([^?=&]+)(=([^&]*))?/g, (_$0: string, $1: string | undefined, _$2: string | undefined, $3: string | undefined) => { + if ($1 !== undefined && $3 !== undefined) { + queryString[$1] = decodeURIComponent($3); + } + return $3 ?? ''; + }); + + const name: string = section[1].replace(/rel="(.*)"/, '$1').trim(); + return [name, queryString]; + }), + ); + } + + /** + * Method to parse the links + */ + parse(header: string): { [key: string]: number } { + const sections = this.parseAll(header); + const links: { [key: string]: number } = {}; + for (const [name, queryParams] of Object.entries(sections)) { + if (queryParams?.page !== undefined) { + links[name] = parseInt(queryParams.page, 10); + } + } + return links; + } +} diff --git a/src/main/webapp/app/entities/activity/activity.model.ts b/src/main/webapp/app/entities/activity/activity.model.ts new file mode 100644 index 0000000..b92fc3f --- /dev/null +++ b/src/main/webapp/app/entities/activity/activity.model.ts @@ -0,0 +1,16 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +// All Entities that are Activity enabled, must extend this interface. +export interface IActivityDomain extends IResilientEntity { + // Marker property to check if a domain implements this interface + // To check at runtime just do: .implementsIActivityDomain === true + implementsIActivityDomain?: true; + + activityDomainClass?: string; + activityProgressKey?: string; + state?: any; +} + +export interface MyInterface { + marker: true; // Marker property +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/admin/authority/authority.model.ts b/src/main/webapp/app/entities/admin/authority/authority.model.ts new file mode 100644 index 0000000..aaf91d2 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/authority.model.ts @@ -0,0 +1,5 @@ +export interface IAuthority { + name: string; +} + +export type NewAuthority = Omit & { name: null }; diff --git a/src/main/webapp/app/entities/admin/authority/authority.routes.ts b/src/main/webapp/app/entities/admin/authority/authority.routes.ts new file mode 100644 index 0000000..d9ff3e4 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/authority.routes.ts @@ -0,0 +1,42 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { AuthorityComponent } from './list/authority.component'; +import { AuthorityDetailComponent } from './detail/authority-detail.component'; +import { AuthorityUpdateComponent } from './update/authority-update.component'; +import AuthorityResolve from './route/authority-routing-resolve.service'; + +const authorityRoute: Routes = [ + { + path: '', + component: AuthorityComponent, + data: { + authorities: ['ROLE_ADMIN'], + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':name/view', + component: AuthorityDetailComponent, + resolve: { + authority: AuthorityResolve, + }, + data: { + authorities: ['ROLE_ADMIN'], + }, + canActivate: [UserRouteAccessService], + }, + { + path: 'new', + component: AuthorityUpdateComponent, + resolve: { + authority: AuthorityResolve, + }, + data: { + authorities: ['ROLE_ADMIN'], + }, + canActivate: [UserRouteAccessService], + }, +]; + +export default authorityRoute; diff --git a/src/main/webapp/app/entities/admin/authority/authority.test-samples.ts b/src/main/webapp/app/entities/admin/authority/authority.test-samples.ts new file mode 100644 index 0000000..78bc2d0 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/authority.test-samples.ts @@ -0,0 +1,22 @@ +import { IAuthority, NewAuthority } from './authority.model'; + +export const sampleWithRequiredData: IAuthority = { + name: 'bd13a3a6-37aa-4ff5-b443-87e414bb29c4', +}; + +export const sampleWithPartialData: IAuthority = { + name: '21806b5f-62c4-4a2c-9055-e206646e03ae', +}; + +export const sampleWithFullData: IAuthority = { + name: '493bd723-0a03-4253-8474-4ada190dbc11', +}; + +export const sampleWithNewData: NewAuthority = { + name: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.html b/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.html new file mode 100644 index 0000000..537f60e --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (authority) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.spec.ts new file mode 100644 index 0000000..257fd2e --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { AuthorityService } from '../service/authority.service'; + +import { AuthorityDeleteDialogComponent } from './authority-delete-dialog.component'; + +describe('Authority Management Delete Component', () => { + let comp: AuthorityDeleteDialogComponent; + let fixture: ComponentFixture; + let service: AuthorityService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, AuthorityDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(AuthorityDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(AuthorityDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(AuthorityService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete('ABC'); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith('ABC'); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.ts b/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.ts new file mode 100644 index 0000000..9aa663c --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/delete/authority-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IAuthority } from '../authority.model'; +import { AuthorityService } from '../service/authority.service'; + +@Component({ + standalone: true, + templateUrl: './authority-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class AuthorityDeleteDialogComponent { + authority?: IAuthority; + + protected authorityService = inject(AuthorityService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: string): void { + this.authorityService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.html b/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.html new file mode 100644 index 0000000..cf9eb72 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.html @@ -0,0 +1,26 @@ +
+
+ @if (authority()) { +
+

Authority

+ +
+ + + + + +
+
Name
+
+ {{ authority()!.name }} +
+
+ + +
+ } +
+
diff --git a/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.spec.ts b/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.spec.ts new file mode 100644 index 0000000..1e8e065 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { AuthorityDetailComponent } from './authority-detail.component'; + +describe('Authority Management Detail Component', () => { + let comp: AuthorityDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AuthorityDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: AuthorityDetailComponent, + resolve: { authority: () => of({ name: 'ABC' }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(AuthorityDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AuthorityDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load authority on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', AuthorityDetailComponent); + + // THEN + expect(instance.authority()).toEqual(expect.objectContaining({ name: 'ABC' })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.ts b/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.ts new file mode 100644 index 0000000..c054dd0 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/detail/authority-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IAuthority } from '../authority.model'; + +@Component({ + standalone: true, + selector: 'jhi-authority-detail', + templateUrl: './authority-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class AuthorityDetailComponent { + authority = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/admin/authority/list/authority.component.html b/src/main/webapp/app/entities/admin/authority/list/authority.component.html new file mode 100644 index 0000000..5efbc7a --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/list/authority.component.html @@ -0,0 +1,73 @@ +
+

+ Authorities + +
+ + + +
+

+ + + + + + @if (authorities?.length === 0) { +
+ Nenhum Authorities encontrado +
+ } + + @if (authorities && authorities.length > 0) { +
+ + + + + + + + + @for (authority of authorities; track trackName) { + + + + + } + +
+
+ Name + + +
+
+ {{ authority.name }} + +
+ + + Visualizar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/admin/authority/list/authority.component.spec.ts b/src/main/webapp/app/entities/admin/authority/list/authority.component.spec.ts new file mode 100644 index 0000000..7b05543 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/list/authority.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../authority.test-samples'; +import { AuthorityService } from '../service/authority.service'; + +import { AuthorityComponent } from './authority.component'; +import SpyInstance = jest.SpyInstance; + +describe('Authority Management Component', () => { + let comp: AuthorityComponent; + let fixture: ComponentFixture; + let service: AuthorityService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, AuthorityComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'name,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'name,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'name,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(AuthorityComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(AuthorityComponent); + comp = fixture.componentInstance; + service = TestBed.inject(AuthorityService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ name: 'ABC' }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ name: 'CBA' }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.authorities?.[0]).toEqual(expect.objectContaining({ name: 'ABC' })); + }); + + describe('trackName', () => { + it('Should forward to authorityService', () => { + const entity = { name: 'ABC' }; + jest.spyOn(service, 'getAuthorityIdentifier'); + const name = comp.trackName(0, entity); + expect(service.getAuthorityIdentifier).toHaveBeenCalledWith(entity); + expect(name).toBe(entity.name); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['name,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/admin/authority/list/authority.component.ts b/src/main/webapp/app/entities/admin/authority/list/authority.component.ts new file mode 100644 index 0000000..1a3ad0e --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/list/authority.component.ts @@ -0,0 +1,121 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IAuthority } from '../authority.model'; +import { EntityArrayResponseType, AuthorityService } from '../service/authority.service'; +import { AuthorityDeleteDialogComponent } from '../delete/authority-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-authority', + templateUrl: './authority.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class AuthorityComponent implements OnInit { + subscription: Subscription | null = null; + authorities?: IAuthority[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected authorityService = inject(AuthorityService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackName = (_index: number, item: IAuthority): string => this.authorityService.getAuthorityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.authorities || this.authorities.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(authority: IAuthority): void { + const modalRef = this.modalService.open(AuthorityDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.authority = authority; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.authorities = this.refineData(dataFromBody); + } + + protected refineData(data: IAuthority[]): IAuthority[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IAuthority[] | null): IAuthority[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.authorityService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/admin/authority/route/authority-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/admin/authority/route/authority-routing-resolve.service.spec.ts new file mode 100644 index 0000000..4b7d844 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/route/authority-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IAuthority } from '../authority.model'; +import { AuthorityService } from '../service/authority.service'; + +import authorityResolve from './authority-routing-resolve.service'; + +describe('Authority routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: AuthorityService; + let resultAuthority: IAuthority | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(AuthorityService); + resultAuthority = undefined; + }); + + describe('resolve', () => { + it('should return IAuthority returned by find', () => { + // GIVEN + service.find = jest.fn(name => of(new HttpResponse({ body: { name } }))); + mockActivatedRouteSnapshot.params = { name: 'ABC' }; + + // WHEN + TestBed.runInInjectionContext(() => { + authorityResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultAuthority = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith('ABC'); + expect(resultAuthority).toEqual({ name: 'ABC' }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + authorityResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultAuthority = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultAuthority).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { name: 'ABC' }; + + // WHEN + TestBed.runInInjectionContext(() => { + authorityResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultAuthority = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith('ABC'); + expect(resultAuthority).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/admin/authority/route/authority-routing-resolve.service.ts b/src/main/webapp/app/entities/admin/authority/route/authority-routing-resolve.service.ts new file mode 100644 index 0000000..9b9c7e2 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/route/authority-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IAuthority } from '../authority.model'; +import { AuthorityService } from '../service/authority.service'; + +const authorityResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['name']; + if (id) { + return inject(AuthorityService) + .find(id) + .pipe( + mergeMap((authority: HttpResponse) => { + if (authority.body) { + return of(authority.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default authorityResolve; diff --git a/src/main/webapp/app/entities/admin/authority/service/authority.service.spec.ts b/src/main/webapp/app/entities/admin/authority/service/authority.service.spec.ts new file mode 100644 index 0000000..fd8b639 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/service/authority.service.spec.ts @@ -0,0 +1,180 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IAuthority } from '../authority.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../authority.test-samples'; + +import { AuthorityService } from './authority.service'; + +const requireRestSample: IAuthority = { + ...sampleWithRequiredData, +}; + +describe('Authority Service', () => { + let service: AuthorityService; + let httpMock: HttpTestingController; + let expectedResult: IAuthority | IAuthority[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(AuthorityService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find('ABC').subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a Authority', () => { + const authority = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(authority).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of Authority', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a Authority', () => { + const expected = true; + + service.delete('ABC').subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addAuthorityToCollectionIfMissing', () => { + it('should add a Authority to an empty array', () => { + const authority: IAuthority = sampleWithRequiredData; + expectedResult = service.addAuthorityToCollectionIfMissing([], authority); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(authority); + }); + + it('should not add a Authority to an array that contains it', () => { + const authority: IAuthority = sampleWithRequiredData; + const authorityCollection: IAuthority[] = [ + { + ...authority, + }, + sampleWithPartialData, + ]; + expectedResult = service.addAuthorityToCollectionIfMissing(authorityCollection, authority); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a Authority to an array that doesn't contain it", () => { + const authority: IAuthority = sampleWithRequiredData; + const authorityCollection: IAuthority[] = [sampleWithPartialData]; + expectedResult = service.addAuthorityToCollectionIfMissing(authorityCollection, authority); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(authority); + }); + + it('should add only unique Authority to an array', () => { + const authorityArray: IAuthority[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const authorityCollection: IAuthority[] = [sampleWithRequiredData]; + expectedResult = service.addAuthorityToCollectionIfMissing(authorityCollection, ...authorityArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const authority: IAuthority = sampleWithRequiredData; + const authority2: IAuthority = sampleWithPartialData; + expectedResult = service.addAuthorityToCollectionIfMissing([], authority, authority2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(authority); + expect(expectedResult).toContain(authority2); + }); + + it('should accept null and undefined values', () => { + const authority: IAuthority = sampleWithRequiredData; + expectedResult = service.addAuthorityToCollectionIfMissing([], null, authority, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(authority); + }); + + it('should return initial array if no Authority is added', () => { + const authorityCollection: IAuthority[] = [sampleWithRequiredData]; + expectedResult = service.addAuthorityToCollectionIfMissing(authorityCollection, undefined, null); + expect(expectedResult).toEqual(authorityCollection); + }); + }); + + describe('compareAuthority', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareAuthority(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { name: 'ABC' }; + const entity2 = null; + + const compareResult1 = service.compareAuthority(entity1, entity2); + const compareResult2 = service.compareAuthority(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { name: 'ABC' }; + const entity2 = { name: 'CBA' }; + + const compareResult1 = service.compareAuthority(entity1, entity2); + const compareResult2 = service.compareAuthority(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { name: 'ABC' }; + const entity2 = { name: 'ABC' }; + + const compareResult1 = service.compareAuthority(entity1, entity2); + const compareResult2 = service.compareAuthority(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/admin/authority/service/authority.service.ts b/src/main/webapp/app/entities/admin/authority/service/authority.service.ts new file mode 100644 index 0000000..0f9291f --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/service/authority.service.ts @@ -0,0 +1,64 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IAuthority, NewAuthority } from '../authority.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class AuthorityService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/authorities'); + + create(authority: NewAuthority): Observable { + return this.http.post(this.resourceUrl, authority, { observe: 'response' }); + } + + find(id: string): Observable { + return this.http.get(`${this.resourceUrl}/${id}`, { observe: 'response' }); + } + + query(req?: any): Observable { + const options = createRequestOption(req); + return this.http.get(this.resourceUrl, { params: options, observe: 'response' }); + } + + delete(id: string): Observable> { + return this.http.delete(`${this.resourceUrl}/${id}`, { observe: 'response' }); + } + + getAuthorityIdentifier(authority: Pick): string { + return authority.name; + } + + compareAuthority(o1: Pick | null, o2: Pick | null): boolean { + return o1 && o2 ? this.getAuthorityIdentifier(o1) === this.getAuthorityIdentifier(o2) : o1 === o2; + } + + addAuthorityToCollectionIfMissing>( + authorityCollection: Type[], + ...authoritiesToCheck: (Type | null | undefined)[] + ): Type[] { + const authorities: Type[] = authoritiesToCheck.filter(isPresent); + if (authorities.length > 0) { + const authorityCollectionIdentifiers = authorityCollection.map(authorityItem => this.getAuthorityIdentifier(authorityItem)); + const authoritiesToAdd = authorities.filter(authorityItem => { + const authorityIdentifier = this.getAuthorityIdentifier(authorityItem); + if (authorityCollectionIdentifiers.includes(authorityIdentifier)) { + return false; + } + authorityCollectionIdentifiers.push(authorityIdentifier); + return true; + }); + return [...authoritiesToAdd, ...authorityCollection]; + } + return authorityCollection; + } +} diff --git a/src/main/webapp/app/entities/admin/authority/update/authority-form.service.spec.ts b/src/main/webapp/app/entities/admin/authority/update/authority-form.service.spec.ts new file mode 100644 index 0000000..98b5041 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/update/authority-form.service.spec.ts @@ -0,0 +1,64 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../authority.test-samples'; + +import { AuthorityFormService } from './authority-form.service'; + +describe('Authority Form Service', () => { + let service: AuthorityFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthorityFormService); + }); + + describe('Service methods', () => { + describe('createAuthorityFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createAuthorityFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + name: expect.any(Object), + }), + ); + }); + + it('passing IAuthority should create a new form with FormGroup', () => { + const formGroup = service.createAuthorityFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + name: expect.any(Object), + }), + ); + }); + }); + + describe('getAuthority', () => { + it('should return NewAuthority for default Authority initial value', () => { + const formGroup = service.createAuthorityFormGroup(sampleWithNewData); + + const authority = service.getAuthority(formGroup) as any; + + expect(authority).toMatchObject(sampleWithNewData); + }); + + it('should return NewAuthority for empty Authority initial value', () => { + const formGroup = service.createAuthorityFormGroup(); + + const authority = service.getAuthority(formGroup) as any; + + expect(authority).toMatchObject({}); + }); + + it('should return IAuthority', () => { + const formGroup = service.createAuthorityFormGroup(sampleWithRequiredData); + + const authority = service.getAuthority(formGroup) as any; + + expect(authority).toMatchObject(sampleWithRequiredData); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/admin/authority/update/authority-form.service.ts b/src/main/webapp/app/entities/admin/authority/update/authority-form.service.ts new file mode 100644 index 0000000..653ee49 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/update/authority-form.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IAuthority, NewAuthority } from '../authority.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { name: T['name'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IAuthority for edit and NewAuthorityFormGroupInput for create. + */ +type AuthorityFormGroupInput = IAuthority | PartialWithRequiredKeyOf; + +type AuthorityFormDefaults = Pick; + +type AuthorityFormGroupContent = { + name: FormControl; +}; + +export type AuthorityFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class AuthorityFormService { + createAuthorityFormGroup(authority: AuthorityFormGroupInput = { name: null }): AuthorityFormGroup { + const authorityRawValue = { + ...this.getFormDefaults(), + ...authority, + }; + return new FormGroup({ + name: new FormControl( + { value: authorityRawValue.name, disabled: authorityRawValue.name !== null }, + { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(50)], + }, + ), + }); + } + + getAuthority(form: AuthorityFormGroup): NewAuthority { + return form.getRawValue() as NewAuthority; + } + + resetForm(form: AuthorityFormGroup, authority: AuthorityFormGroupInput): void { + const authorityRawValue = { ...this.getFormDefaults(), ...authority }; + form.reset( + { + ...authorityRawValue, + name: { value: authorityRawValue.name, disabled: authorityRawValue.name !== null }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): AuthorityFormDefaults { + return { + name: null, + }; + } +} diff --git a/src/main/webapp/app/entities/admin/authority/update/authority-update.component.html b/src/main/webapp/app/entities/admin/authority/update/authority-update.component.html new file mode 100644 index 0000000..e05def9 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/update/authority-update.component.html @@ -0,0 +1,58 @@ +
+
+
+

+ Criar ou editar Authority +

+ +
+ + +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('name')?.errors?.maxlength) { + Este campo não pode ter mais que 50 caracteres. + } +
+ } +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/admin/authority/update/authority-update.component.spec.ts b/src/main/webapp/app/entities/admin/authority/update/authority-update.component.spec.ts new file mode 100644 index 0000000..d5c09d9 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/update/authority-update.component.spec.ts @@ -0,0 +1,80 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { AuthorityService } from '../service/authority.service'; +import { IAuthority } from '../authority.model'; +import { AuthorityFormService } from './authority-form.service'; + +import { AuthorityUpdateComponent } from './authority-update.component'; + +describe('Authority Management Update Component', () => { + let comp: AuthorityUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let authorityFormService: AuthorityFormService; + let authorityService: AuthorityService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, AuthorityUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(AuthorityUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(AuthorityUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + authorityFormService = TestBed.inject(AuthorityFormService); + authorityService = TestBed.inject(AuthorityService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should update editForm', () => { + const authority: IAuthority = { name: 'CBA' }; + + activatedRoute.data = of({ authority }); + comp.ngOnInit(); + + expect(comp.authority).toEqual(authority); + }); + }); + + describe('save', () => { + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const authority = { name: 'ABC' }; + jest.spyOn(authorityFormService, 'getAuthority').mockReturnValue({ name: null }); + jest.spyOn(authorityService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ authority: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: authority })); + saveSubject.complete(); + + // THEN + expect(authorityFormService.getAuthority).toHaveBeenCalled(); + expect(authorityService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/admin/authority/update/authority-update.component.ts b/src/main/webapp/app/entities/admin/authority/update/authority-update.component.ts new file mode 100644 index 0000000..1ff40b5 --- /dev/null +++ b/src/main/webapp/app/entities/admin/authority/update/authority-update.component.ts @@ -0,0 +1,73 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IAuthority } from '../authority.model'; +import { AuthorityService } from '../service/authority.service'; +import { AuthorityFormService, AuthorityFormGroup } from './authority-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-authority-update', + templateUrl: './authority-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class AuthorityUpdateComponent implements OnInit { + isSaving = false; + authority: IAuthority | null = null; + + protected authorityService = inject(AuthorityService); + protected authorityFormService = inject(AuthorityFormService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: AuthorityFormGroup = this.authorityFormService.createAuthorityFormGroup(); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ authority }) => { + this.authority = authority; + if (authority) { + this.updateForm(authority); + } + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const authority = this.authorityFormService.getAuthority(this.editForm); + this.subscribeToSaveResponse(this.authorityService.create(authority)); + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(authority: IAuthority): void { + this.authority = authority; + this.authorityFormService.resetForm(this.editForm, authority); + } +} diff --git a/src/main/webapp/app/entities/content-page/content-page.model.ts b/src/main/webapp/app/entities/content-page/content-page.model.ts new file mode 100644 index 0000000..0c23514 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/content-page.model.ts @@ -0,0 +1,11 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { ContentPageType } from 'app/entities/enumerations/content-page-type.model'; + +export interface IContentPage extends IResilientEntity { + title?: string | null; + slug?: keyof typeof ContentPageType | null; + content?: string | null; +} + +export type NewContentPage = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/content-page/content-page.routes.ts b/src/main/webapp/app/entities/content-page/content-page.routes.ts new file mode 100644 index 0000000..68f061a --- /dev/null +++ b/src/main/webapp/app/entities/content-page/content-page.routes.ts @@ -0,0 +1,37 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { ContentPageComponent } from './list/content-page.component'; +import { ContentPageDetailComponent } from './detail/content-page-detail.component'; +import { ContentPageUpdateComponent } from './update/content-page-update.component'; +import ContentPageResolve from './route/content-page-routing-resolve.service'; + +const contentPageRoute: Routes = [ + { + path: '', + component: ContentPageComponent, + data: { + defaultSort: 'id,' + ASC, + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':id/view', + component: ContentPageDetailComponent, + resolve: { + contentPage: ContentPageResolve, + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':id/edit', + component: ContentPageUpdateComponent, + resolve: { + contentPage: ContentPageResolve, + }, + canActivate: [UserRouteAccessService], + }, +]; + +export default contentPageRoute; diff --git a/src/main/webapp/app/entities/content-page/detail/content-page-detail.component.html b/src/main/webapp/app/entities/content-page/detail/content-page-detail.component.html new file mode 100644 index 0000000..8fd8317 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/detail/content-page-detail.component.html @@ -0,0 +1,40 @@ +
+
+ @if (contentPage()) { +
+

Content Page

+ +
+ + + + + +
+
Title
+
+ {{ contentPage()!.title }} +
+
Slug
+
+ {{ + { null: '', HOME_ANONYMOUS: 'Home Anonymous', HOME_AUTHENTICATED: 'Home Authenticated' }[contentPage()!.slug ?? 'null'] + }} +
+
Content
+
+ {{ contentPage()!.content }} +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/content-page/detail/content-page-detail.component.ts b/src/main/webapp/app/entities/content-page/detail/content-page-detail.component.ts new file mode 100644 index 0000000..e2e1f05 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/detail/content-page-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IContentPage } from '../content-page.model'; + +@Component({ + standalone: true, + selector: 'jhi-content-page-detail', + templateUrl: './content-page-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class ContentPageDetailComponent { + contentPage = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/content-page/list/content-page.component.html b/src/main/webapp/app/entities/content-page/list/content-page.component.html new file mode 100644 index 0000000..e784980 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/list/content-page.component.html @@ -0,0 +1,73 @@ +
+

+ Content Pages + +
+ +
+

+ + + + + + @if (contentPages?.length === 0) { +
+ Nenhum Content Page encontrado +
+ } + + @if (contentPages && contentPages.length > 0) { +
+ + + + + + + + + + + @for (contentPage of contentPages; track trackId) { + + + + + + + } + +
+
+ Title + + +
+
+
+ Slug + + +
+
{{ contentPage.title }} + {{ { null: '', HOME_ANONYMOUS: 'Home Anonymous', HOME_AUTHENTICATED: 'Home Authenticated'}[contentPage.slug ?? 'null'] }} + + +
+
+ } +
diff --git a/src/main/webapp/app/entities/content-page/list/content-page.component.ts b/src/main/webapp/app/entities/content-page/list/content-page.component.ts new file mode 100644 index 0000000..f160e2e --- /dev/null +++ b/src/main/webapp/app/entities/content-page/list/content-page.component.ts @@ -0,0 +1,110 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IContentPage } from '../content-page.model'; +import { EntityArrayResponseType, ContentPageService } from '../service/content-page.service'; + +import { ResilientBaseComponent } from 'app/resilient/resilient-base/resilient-base.component'; + +@Component({ + standalone: true, + selector: 'jhi-content-page', + templateUrl: './content-page.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class ContentPageComponent extends ResilientBaseComponent implements OnInit { + subscription: Subscription | null = null; + contentPages?: IContentPage[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected contentPageService = inject(ContentPageService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IContentPage): number => this.contentPageService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.contentPages || this.contentPages.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.contentPages = this.refineData(dataFromBody); + } + + protected refineData(data: IContentPage[]): IContentPage[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IContentPage[] | null): IContentPage[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.contentPageService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/content-page/route/content-page-routing-resolve.service.ts b/src/main/webapp/app/entities/content-page/route/content-page-routing-resolve.service.ts new file mode 100644 index 0000000..b57d519 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/route/content-page-routing-resolve.service.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IContentPage } from '../content-page.model'; +import { ContentPageService } from '../service/content-page.service'; + +const contentPageResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(ContentPageService) + .find(id) + .pipe( + mergeMap((contentPage: HttpResponse) => { + if (contentPage.body) { + const ut = of(contentPage.body); + return of(contentPage.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default contentPageResolve; diff --git a/src/main/webapp/app/entities/content-page/service/content-page.service.ts b/src/main/webapp/app/entities/content-page/service/content-page.service.ts new file mode 100644 index 0000000..c3cd3cd --- /dev/null +++ b/src/main/webapp/app/entities/content-page/service/content-page.service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IContentPage, NewContentPage } from '../content-page.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class ContentPageService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/content-pages'); + getResourceUrl(): string { return this.resourceUrl; } +} diff --git a/src/main/webapp/app/entities/content-page/update/content-page-form.service.ts b/src/main/webapp/app/entities/content-page/update/content-page-form.service.ts new file mode 100644 index 0000000..d37c7c5 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/update/content-page-form.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IContentPage, NewContentPage } from '../content-page.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IContentPage for edit and NewContentPageFormGroupInput for create. + */ +type ContentPageFormGroupInput = IContentPage | PartialWithRequiredKeyOf; + +type ContentPageFormDefaults = Pick; + +type ContentPageFormGroupContent = { + id: FormControl; + title: FormControl; + slug: FormControl; + content: FormControl; + version: FormControl; +}; + +export type ContentPageFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class ContentPageFormService { + createContentPageFormGroup(contentPage: ContentPageFormGroupInput = { id: null }): ContentPageFormGroup { + const contentPageRawValue = { + ...this.getFormDefaults(), + ...contentPage, + }; + return new FormGroup({ + id: new FormControl( + { value: contentPageRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + title: new FormControl(contentPageRawValue.title, { + validators: [Validators.required], + }), + slug: new FormControl(contentPageRawValue.slug, { + validators: [Validators.required], + }), + content: new FormControl(contentPageRawValue.content, { + validators: [Validators.required], + }), + version: new FormControl(contentPageRawValue.version), + }); + } + + getContentPage(form: ContentPageFormGroup): IContentPage | NewContentPage { + return form.getRawValue() as IContentPage | NewContentPage; + } + + resetForm(form: ContentPageFormGroup, contentPage: ContentPageFormGroupInput): void { + const contentPageRawValue = { ...this.getFormDefaults(), ...contentPage }; + form.reset( + { + ...contentPageRawValue, + id: { value: contentPageRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): ContentPageFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/content-page/update/content-page-update.component.html b/src/main/webapp/app/entities/content-page/update/content-page-update.component.html new file mode 100644 index 0000000..f1e7d09 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/update/content-page-update.component.html @@ -0,0 +1,64 @@ +
+
+
+

+ Editar Unit Type +

+ +
+ +
+ + {{ + { null: '', HOME_ANONYMOUS: 'Home Anonymous', HOME_AUTHENTICATED: 'Home Authenticated' }[editForm.get('slug')!.value ?? 'null'] + }} +
+
+ + + @if (editForm.get('title')!.invalid && (editForm.get('title')!.dirty || editForm.get('title')!.touched)) { +
+ @if (editForm.get('title')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + + + @if (editForm.get('content')!.invalid && (editForm.get('content')!.dirty || editForm.get('content')!.touched)) { +
+ @if (editForm.get('content')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ +
+ + + @if (editForm.controls.id.value !== null) { + } + +
+
+
+
diff --git a/src/main/webapp/app/entities/content-page/update/content-page-update.component.ts b/src/main/webapp/app/entities/content-page/update/content-page-update.component.ts new file mode 100644 index 0000000..7fcaee4 --- /dev/null +++ b/src/main/webapp/app/entities/content-page/update/content-page-update.component.ts @@ -0,0 +1,126 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { EditorModule } from '@tinymce/tinymce-angular'; + +import { ResilientBaseComponent } from 'app/resilient/resilient-base/resilient-base.component'; + +import { ContentPageType } from 'app/entities/enumerations/content-page-type.model'; +import { IContentPage } from '../content-page.model'; +import { ContentPageService } from '../service/content-page.service'; +import { ContentPageFormService, ContentPageFormGroup } from './content-page-form.service'; +import { Editor } from 'content/tinymce/tinymce'; + +@Component({ + standalone: true, + selector: 'jhi-content-page-update', + templateUrl: './content-page-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule, EditorModule], +}) +export class ContentPageUpdateComponent extends ResilientBaseComponent implements OnInit { + isSaving = false; + contentPage: IContentPage | null = null; + contentPageTypeValues = Object.keys(ContentPageType); + + /* TinyMCE Html Editor config options */ + editorConfig = { + height: 500, + menubar: false, + image_dimensions: true, // disables the width/height input fields + automatic_uploads: false, + setup: (editor: any) => { + // Clean up default width/height if they were added automatically + editor.on('BeforeSetContent', (e: any) => { + if (e.content.includes(' { + this.contentPage = contentPage; + if (contentPage) { + this.updateForm(contentPage); + } + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const contentPage = this.contentPageFormService.getContentPage(this.editForm); + if (contentPage.id !== null) { + this.subscribeToSaveResponse(this.contentPageService.update(contentPage)); + } else { + // TODO : Throw exception. Creation is NOT allowed + this.subscribeToSaveResponse(this.contentPageService.create(contentPage)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(contentPage: IContentPage): void { + this.contentPage = contentPage; + this.contentPageFormService.resetForm(this.editForm, contentPage); + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/dashboard-component-detail-value.model.ts b/src/main/webapp/app/entities/dashboard-component/dashboard-component-detail-value.model.ts new file mode 100644 index 0000000..f565901 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/dashboard-component-detail-value.model.ts @@ -0,0 +1,10 @@ +import { IOrganization } from 'app/entities/organization/organization.model'; +import { IDashboardComponentDetail } from './dashboard-component-detail.model'; +import { IDashboardComponent } from './dashboard-component.model'; + +export interface IDashboardComponentDetailValue { + dashboardComponentDetail?: IDashboardComponentDetail | null; + value?: number | null; + organization?: Pick | null; + targetDashboardComponent?: Pick | null; +} diff --git a/src/main/webapp/app/entities/dashboard-component/dashboard-component-detail.model.ts b/src/main/webapp/app/entities/dashboard-component/dashboard-component-detail.model.ts new file mode 100644 index 0000000..d712346 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/dashboard-component-detail.model.ts @@ -0,0 +1,16 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IDashboardComponent } from './dashboard-component.model'; +import { IVariable } from 'app/entities/variable/variable.model'; + +export interface IDashboardComponentDetail extends IResilientEntity { + name?: string | null; + color?: string | null; + indexOrder?: number | null; + help?: string | null; + dashboardComponent?: Pick | null; + targetDashboardComponent?: Pick | null; + variable?: Pick | null; +} + +export type NewDashboardComponentDetail = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/dashboard-component/dashboard-component-organization.model.ts b/src/main/webapp/app/entities/dashboard-component/dashboard-component-organization.model.ts new file mode 100644 index 0000000..5d12810 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/dashboard-component-organization.model.ts @@ -0,0 +1,11 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IDashboardComponent } from './dashboard-component.model'; +import { IOrganization } from '../organization/organization.model'; + +export interface IDashboardComponentOrganization extends IResilientEntity { + dashboardComponent?: Pick | null; + organization?: Pick | null; +} + +export type NewDashboardComponentOrganization = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/dashboard-component/dashboard-component.model.ts b/src/main/webapp/app/entities/dashboard-component/dashboard-component.model.ts new file mode 100644 index 0000000..36c7f66 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/dashboard-component.model.ts @@ -0,0 +1,22 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; +import { DashboardComponentType } from 'app/entities/enumerations/dashboard-component-type.model'; +import { DashboardComponentChartType } from 'app/entities/enumerations/dashboard-component-chart-type.model'; +import { DashboardComponentView } from 'app/entities/enumerations/dashboard-component-view.model'; + +import { IDashboardComponentDetail } from './dashboard-component-detail.model'; +import { IDashboardComponentOrganization } from './dashboard-component-organization.model'; + +export interface IDashboardComponent extends IResilientEntity { + code?: string | null; + name?: string | null; + description?: string | null; + isActive?: boolean | null; + isExportable?: boolean | null; + type?: keyof typeof DashboardComponentType | null; + view?: keyof typeof DashboardComponentView | null; + chartType?: keyof typeof DashboardComponentChartType | null; + dashboardComponentDetails?: IDashboardComponentDetail[] | null; + dashboardComponentOrganizations?: IDashboardComponentOrganization[] | null; +} + +export type NewDashboardComponent = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/dashboard-component/dashboard-component.routes.ts b/src/main/webapp/app/entities/dashboard-component/dashboard-component.routes.ts new file mode 100644 index 0000000..e332954 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/dashboard-component.routes.ts @@ -0,0 +1,79 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { DashboardComponentComponent } from './list/dashboard-component.component'; +import { DashboardComponentPreviewComponent } from './preview/dashboard-component-preview.component'; +import { DashboardComponentDetailComponent } from './detail/dashboard-component-detail.component'; +import { DashboardComponentUpdateComponent } from './update/dashboard-component-update.component'; +import DashboardComponentResolve from './route/dashboard-component-routing-resolve.service'; +import { DashboardComponentService } from './service/dashboard-component.service'; +import { SecurityAction} from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; + +const dashboardComponentRoute: Routes = [ + { + path: '', + component: DashboardComponentComponent, + data: { + defaultSort: 'id,' + ASC, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':id/view', + component: DashboardComponentDetailComponent, + resolve: { + dashboardComponent: DashboardComponentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: DashboardComponentService, + resource: Resources.DASHBOARD_COMPONENT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/preview', + component: DashboardComponentPreviewComponent, + resolve: { + dashboardComponent: DashboardComponentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: DashboardComponentUpdateComponent, + resolve: { + dashboardComponent: DashboardComponentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: DashboardComponentService, + resource: Resources.DASHBOARD_COMPONENT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: DashboardComponentUpdateComponent, + resolve: { + dashboardComponent: DashboardComponentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: DashboardComponentService, + resource: Resources.DASHBOARD_COMPONENT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default dashboardComponentRoute; diff --git a/src/main/webapp/app/entities/dashboard-component/dashboard-component.test-samples.ts b/src/main/webapp/app/entities/dashboard-component/dashboard-component.test-samples.ts new file mode 100644 index 0000000..98cdab2 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/dashboard-component.test-samples.ts @@ -0,0 +1,36 @@ +import { IDashboardComponent, NewDashboardComponent } from './dashboard-component.model'; + +export const sampleWithRequiredData: IDashboardComponent = { + id: 19315, + code: '1234' + name: 'except barring upset', + isActive: true, +}; + +export const sampleWithPartialData: IDashboardComponent = { + id: 22736, + code: '1235' + description: 'upon push deserted', + isActive: false, +}; + +export const sampleWithFullData: IDashboardComponent = { + id: 579, + code: '1236' + name: 'tatami', + description: 'barring really', + isActive: true, +}; + +export const sampleWithNewData: NewDashboardComponent = { + code: '1237' + name: 'reckless outgun', + description: 'though', + isActive: false, + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.html b/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.html new file mode 100644 index 0000000..776a61a --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (dashboardComponent) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.spec.ts new file mode 100644 index 0000000..b367902 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { DashboardComponentService } from '../service/dashboard-component.service'; + +import { DashboardComponentDeleteDialogComponent } from './dashboard-component-delete-dialog.component'; + +describe('DashboardComponent Management Delete Component', () => { + let comp: DashboardComponentDeleteDialogComponent; + let fixture: ComponentFixture; + let service: DashboardComponentService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, DashboardComponentDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(DashboardComponentDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(DashboardComponentDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(DashboardComponentService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.ts b/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.ts new file mode 100644 index 0000000..e70b9ad --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/delete/dashboard-component-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IDashboardComponent } from '../dashboard-component.model'; +import { DashboardComponentService } from '../service/dashboard-component.service'; + +@Component({ + standalone: true, + templateUrl: './dashboard-component-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class DashboardComponentDeleteDialogComponent { + dashboardComponent?: IDashboardComponent; + + protected dashboardComponentService = inject(DashboardComponentService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.dashboardComponentService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.html b/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.html new file mode 100644 index 0000000..02c0b06 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.html @@ -0,0 +1,132 @@ + +
+
+ @if (dashboardComponent()) { +
+

Dashboard Component

+ +
+ + + + + +
+
Code
+
+ {{ dashboardComponent()!.code }} +
+
Name
+
+ {{ dashboardComponent()!.name }} +
+
Description
+
+ {{ dashboardComponent()!.description }} +
+
Is Active ?
+
+ {{ dashboardComponent()!.isActive }} +
+
Is Exportable ?
+
+ {{ dashboardComponent()!.isExportable }} +
+ +
+ + + + + + +
+
+ + + + + + + + + + + + + + + +
NameOutput
{{i+1}} + {{ dashboardComponentDetail.name }} + + {{dashboardComponentDetail!.variable!.name}} +
+
+ +
+ + + + + + + + + + + + + +
Organization
{{i+1}} + {{ dashboardComponentOrganization.organization!.code }} +
+
+
+ + + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.spec.ts b/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.spec.ts new file mode 100644 index 0000000..a4a7daf --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { DashboardComponentDetailComponent } from './dashboard-component-detail.component'; + +describe('DashboardComponent Management Detail Component', () => { + let comp: DashboardComponentDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardComponentDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: DashboardComponentDetailComponent, + resolve: { dashboardComponent: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(DashboardComponentDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardComponentDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load dashboardComponent on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', DashboardComponentDetailComponent); + + // THEN + expect(instance.dashboardComponent()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.ts b/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.ts new file mode 100644 index 0000000..d2e38f6 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/detail/dashboard-component-detail.component.ts @@ -0,0 +1,29 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IDashboardComponent, NewDashboardComponent } from '../dashboard-component.model'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; +import { DashboardComponentService } from '../service/dashboard-component.service'; + +@Component({ + standalone: true, + selector: 'jhi-dashboard-component-detail', + templateUrl: './dashboard-component-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class DashboardComponentDetailComponent extends AbstractResilientEntityComponent { + dashboardComponent = input(null); + currentTab: string = 'details'; // default tab + + constructor(service: DashboardComponentService) { super(service); } + + setTab(tabId: string): void { + this.currentTab = tabId; + } + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/factory/dashboard-component-factory.service.ts b/src/main/webapp/app/entities/dashboard-component/factory/dashboard-component-factory.service.ts new file mode 100644 index 0000000..936e17b --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/dashboard-component-factory.service.ts @@ -0,0 +1,38 @@ +import { Injectable, ComponentFactoryResolver, ViewContainerRef, Type } from '@angular/core'; +import { DashboardComponentChart } from './widgets/dashboard-component-chart.component'; +import { DashboardComponentTable } from './widgets/dashboard-component-table.component'; +import { DashboardComponentAccordionTable } from './widgets/dashboard-component-accordion-table.component'; + +@Injectable({ providedIn: 'root' }) +export class DashboardComponentFactoryService { + // ComponentMap with all possible widgets. + // Notes: The key is always lowercase + private componentMap: { [key: string]: Type } = { + chart: DashboardComponentChart, + table: DashboardComponentTable, + accordion: DashboardComponentAccordionTable, + }; + + constructor( + private resolver: ComponentFactoryResolver + ) {} + + /** + * data is a structure: + * { dashboardComponent: } + */ + createComponent(type: string, container: ViewContainerRef, data: any) { + // componentMap key is always lowercase + type = type.toLowerCase(); + + // Get the component to draw + const componentType = this.componentMap[type]; + if (componentType) { + const factory = this.resolver.resolveComponentFactory(componentType); + const componentRef = container.createComponent(factory); + componentRef.instance.dashboardComponent = data.dashboardComponent; // Pass data to component + componentRef.instance.periodVersionId = data.periodVersionId; + componentRef.instance.showTitle = data.showTitle; + } + } +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.css b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.css new file mode 100644 index 0000000..665105b --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.css @@ -0,0 +1,25 @@ +table th:first-child { + min-width: 300px; +} + +/* Align all columns except the first one to the right */ +td:not(:first-child), th:not(:first-child) { + text-align: center; +} + +.dashboard-component-widget hr { + margin-top: 0px; + margin-bottom: 3px; +} + +.dashboard-component-widget h3 { + margin-bottom: 0px; +} + +.dashboard-component-footer { + font-size: 12px; +} + +h5 { + margin-top: 20px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.html b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.html new file mode 100644 index 0000000..844a85e --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.html @@ -0,0 +1,17 @@ +

{{ title }}

+
+ +
+ + \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.ts b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.ts new file mode 100644 index 0000000..0083c5c --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.ts @@ -0,0 +1,294 @@ +/** + * DashboardComponent WIDGET + * + * DashboardComponent component widget to show component output data. Will draw the widget accordantly to the DashboardComponent configuration (TABLE or CHART) + + * Input's: + * - dashboardComponent : The DashboardComponent instance to draw. If present, dashboardComponentId input will be IGNORED. + * - dashboardComponentId: The DashboardComponent id. + * + * Event's: + * + * + * Usage + * - In the intended *.component.html insert the code: + * + * OR + * + * - In the *.component.ts insert the code: + * import { DashboardComponentWidgetComponent } from 'app/entities/dashboard-component/factory/widgets/dashboard-component-widget.component'; + * ... + * add component imports, @Component({ imports: [ DashboardComponentWidgetComponent ] }) + * ... + * _dashboardComponent= input(null); + * OR + * _dashboardComponentId= input(null); + */ + +import { Component, Output, Input, inject, OnInit, EventEmitter } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { map, tap, filter, Subscription, skip } from 'rxjs'; +import dayjs from 'dayjs' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import SharedModule from 'app/shared/shared.module'; +import { IDashboardComponent } from '../../dashboard-component.model'; +import { IDashboardComponentDetailValue } from '../../dashboard-component-detail-value.model'; +import { DashboardComponentService } from '../../service/dashboard-component.service'; +import { NestedAccordionComponent, AccordionItem } from './nested-accordion.component'; +import { ResilientEnvironmentService } from 'app/resilient/resilient-environment/service/resilient-environment.service'; +import { FormatHelper } from 'app/shared/utils/format-helper'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; + +@Component({ + standalone: true, + selector: 'dashboard-component-accordion-table', + templateUrl: './dashboard-component-accordion-table.component.html', + styleUrl: './dashboard-component-accordion-table.component.css', + imports: [SharedModule, RouterModule, NestedAccordionComponent, MatProgressSpinnerModule] +}) +export class DashboardComponentAccordionTable implements OnInit { + @Input() dashboardComponent: IDashboardComponent | null = null; // DashboardComponent instance, if the consumer already loaded a DashboardComponent instance. + @Input() dashboardComponentId: number | null = null; // DashboardComponent Id, if the consumer doesn't have an instance + @Input() periodVersionId: number | null = null; // PeriodVersion Id, needed to retrive the Output data for the component + @Input() showTitle: boolean = false; + + subscriptionWorkOrganization!: Subscription; // Store the subscription + subscriptionPeriod!: Subscription; // Store the subscription to PeriodSelector change + dashboardComponentDetailValues: Map = new Map(); + selectedOrganizationId?: number; + isExporting: boolean = false; + + data: AccordionItem[] = []; + labels: string[] = []; + title: string = "XPTO TITLE"; + + date: string = "myDate"; + version: string = "myVersion"; + periodVersion: IPeriodVersion | null = null; + + protected dashboardComponentService = inject(DashboardComponentService); + protected envService = inject(ResilientEnvironmentService); + protected periodService = inject(PeriodService); + + ngOnInit(): void { + // Get the currently selected ORGANIZATION, by ResilientEnvironmentService + this.subscriptionWorkOrganization = this + .envService + .selectedWorkOrganization + /* .pipe(first()) // READS the Observable only once. Doesn't react to later changes on the subscription */ + .subscribe(org => { + if(org){ + // Ignore if current selected didn't change. This prevents duplication of requests + if (this.selectedOrganizationId != org.id) { + // Set selected + this.selectedOrganizationId = org.id; + + //Reload + this.build(); + } + } + }); + + // Listen to changes in PeriodSelector + this.subscriptionPeriod = this + .envService + .selectedPeriod + .pipe( skip(1)) // Ignore the current value. Just want to react to changes + .subscribe(period => { + if(period){ + // Calculate the latest periodVersionId + this.periodService + .lastVersion(period.id) + .subscribe({ + next: (version: HttpResponse) => { + if (version.body) { + const periodVersion = version.body; + this.periodVersionId = periodVersion.id; + } else { + this.periodVersionId = null; + } + + //Fire Re-Loading of the dashboard + this.build(); + + }, + }); + } else { + // Clear the component. Nothing to show + this.data = []; + } + }); + + // Load periodVersion to show component footer info + if (this.periodVersionId != null) { + this.periodService + .getVersion(this.periodVersionId) + .subscribe({ + next: (version: HttpResponse) => { + if (version.body) { + this.periodVersion = version.body; + } + }, + }); + } + + // If a DashboardComponent was given, just build + if (this.dashboardComponent) { + this.dashboardComponentId = this.dashboardComponent?.id; + this.title = this.dashboardComponent?.name ?? ""; + + this.build(); + } else if (this.dashboardComponentId) { + // Must load the DashboardComponent first, and then build + this.build(); + } else { + // Unable to have a DashboardComponent. EXCEPTION + } + } + + formatDate(date: dayjs.Dayjs | null | undefined): string { + if (!date) { + return ''; + } + + // Ensure `date` is a Day.js object before formatting + const parsedDate = dayjs(date); + + return parsedDate.isValid() ? parsedDate.format('DD/MM/YYYY') : 'Invalid Date'; + } + + private build(): void { + if (this.dashboardComponentId && this.periodVersionId && this.selectedOrganizationId) { + this.dashboardComponentService + .loadOrganizationValuesForComponent(this.selectedOrganizationId, this.dashboardComponentId, this.periodVersionId) + .pipe( + map( + (res: HttpResponse) => { + const data = res.body ?? {}; + + // Check if the data is already a Map or if it's a plain object + if (data instanceof Map) { + return data; + } else { + // Convert object to Map if it's not. + // I had to do it like this, otherwise, it wouldn't be a Map(). + const map = new Map(); + Object.keys(data).forEach(key => { + map.set(key, data[key]); + }); + + const reorderedMap = this.moveTopOrgToLast(map); + + return reorderedMap; + } + } + ) + ) + .subscribe( + (_dashboardComponentDetailValues: Map) => { + this.dashboardComponentDetailValues = _dashboardComponentDetailValues; + this.buildTable(); + } + ); + } else { + //Clear table + this.data = []; + this.labels = []; + } + } + + private moveTopOrgToLast(map: Map): Map { + if (!map.has("1")) return map; // If key "9" doesn't exist, return map as is + + const entries = Array.from(map.entries()); // Convert Map to array of entries + const entryTopOrg = entries.find(([key]) => key === "1"); // Find the entry with key "1" + const filteredEntries = entries.filter(([key]) => key !== "1"); // Remove "9" entry + + if (entryTopOrg) { + filteredEntries.push(entryTopOrg); // Move it to the end + } + + return new Map(filteredEntries); // Create a new Map with updated order + } + + private buildTable(): void { + // Clear previous table + this.labels.length = 0; + this.data.length = 0; + + const variableMap: Map = new Map(); + const variableLabelValues: Map = new Map(); + const variableHelpValues: Map = new Map(); + const variableTargetValues: Map = new Map(); + const variableColorValues: Map = new Map(); + + // First column header. Variable description column + this.labels.push(""); + + // Rebuild + this.dashboardComponentDetailValues.forEach((value, key) => { + let isFirst = true; + + value.forEach(item => { + if (isFirst) this.labels.push(item?.organization?.code || 'undefined'); //Add company label. Get it from the first element. + isFirst = false; + + const variableValues = variableMap.get(item?.dashboardComponentDetail?.variable?.id.toString() || ''); + + if (variableValues !== undefined) { + // Append value to array + variableValues.push(item.value || 0); + } else { + // Insert first entry + variableMap.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', [item.value || 0]); + variableLabelValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.name || 'undefined'); + variableHelpValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.help || ''); + variableColorValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.color || ''); + variableTargetValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.targetDashboardComponent?.id || null); + } + }); + }); + + // Build row array (Label | Value 1 | Value 2 | .. ) + variableMap.forEach((value, key) => { + let newDataRow : AccordionItem = { + id: key, + label: variableLabelValues.get(key) || "", + help: variableHelpValues.get(key) || "", + values: [], + target: variableTargetValues.get(key) || undefined, + loading: false, + expanded: false, + children: undefined, + }; + + value.forEach(val => { + let roundedNumber = FormatHelper.formatDecimalValue(val); + newDataRow.values.push(roundedNumber); + }); + + this.data.push(newDataRow); + }); + + } + + export(): void { + if (this.periodVersionId && this.dashboardComponentId && this.selectedOrganizationId) { + this.isExporting = true; + + this.dashboardComponentService.export(this.selectedOrganizationId, this.dashboardComponentId, this.periodVersionId).subscribe((blob: Blob) => { + const a = document.createElement('a'); + const objectUrl = URL.createObjectURL(blob); + a.href = objectUrl; + a.download = 'report.xlsx'; + a.click(); + URL.revokeObjectURL(objectUrl); + + this.isExporting = false; + }); + } + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.css b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.css new file mode 100644 index 0000000..ec393b3 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.css @@ -0,0 +1,17 @@ +table th:first-child { + min-width: 300px; +} + +/* Align all columns except the first one to the right */ +td:not(:first-child), th:not(:first-child) { + text-align: center; +} + +.dashboard-component-widget hr { + margin-top: 0px; + margin-bottom: 3px; +} + +.dashboard-component-widget h3 { + margin-bottom: 0px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.html b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.html new file mode 100644 index 0000000..0be963b --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.html @@ -0,0 +1,11 @@ + {{ '' // dashboard-component-chart }} + {{ '' // docs say [type] but its WRONG, must be (type) }} +
+

{{ title }}

+
+ + +
\ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.ts b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.ts new file mode 100644 index 0000000..d6d54a7 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component.ts @@ -0,0 +1,215 @@ +/** + * DashboardComponent WIDGET + * + * DashboardComponent component widget to show component output data. Will draw the widget accordantly to the DashboardComponent configuration (TABLE or CHART) + + * Input's: + * - dashboardComponent : The DashboardComponent instance to draw. If present, dashboardComponentId input will be IGNORED. + * - dashboardComponentId: The DashboardComponent id. + * + * Event's: + * + * + * Usage + * - In the intended *.component.html insert the code: + * + * OR + * + * - In the *.component.ts insert the code: + * import { DashboardComponentWidgetComponent } from 'app/entities/dashboard-component/factory/widgets/dashboard-component-widget.component'; + * ... + * add component imports, @Component({ imports: [ DashboardComponentWidgetComponent ] }) + * ... + * _dashboardComponent= input(null); + * OR + * _dashboardComponentId= input(null); + */ + +import { ChangeDetectorRef, Component, Output, Input, inject, OnInit, EventEmitter, AfterViewInit, OnChanges, SimpleChanges } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { map, tap, filter } from 'rxjs'; + +// CHART.JS +import { NgChartsModule } from 'ng2-charts'; +import { ChartOptions, ChartType, ChartData } from 'chart.js'; + +import SharedModule from 'app/shared/shared.module'; +import { IDashboardComponent } from '../../dashboard-component.model'; +import { IDashboardComponentDetailValue } from '../../dashboard-component-detail-value.model'; +import { DashboardComponentService } from '../../service/dashboard-component.service'; + +@Component({ + standalone: true, + selector: 'dashboard-component-chart', + templateUrl: './dashboard-component-chart.component.html', + styleUrl: './dashboard-component-chart.component.css', + imports: [SharedModule, NgChartsModule, RouterModule] +}) +export class DashboardComponentChart implements OnInit, AfterViewInit, OnChanges { + @Input() dashboardComponent: IDashboardComponent | null = null; // DashboardComponent instance, if the consumer already loaded a DashboardComponent instance. + @Input() dashboardComponentId: number | null = null; // DashboardComponent Îd, if the consumer doesn't have an instance + @Input() periodVersionId: number | null = null; // PeriodVersion Id, needed to retrive the Output data for the component + + dashboardComponentDetailValues: Map = new Map(); + title: string = ""; + + protected dashboardComponentService = inject(DashboardComponentService); + + /* CHART variables */ + public barChartOptions: ChartOptions = { + responsive: true, + plugins: { + title: { + display: true, + text: '123 4567 789' + } + } + }; + public barChartLabels: string[] = ['January', 'February', 'March', 'April']; + public chartType: ChartType = 'bar' as ChartType; + + chartData: ChartData<'bar'> = { + labels: [], + datasets: [ + { data: [], label: 'Dataset 1', backgroundColor: 'rgba(75,192,192,0.6)' } + ] + }; + + chartOptions: ChartOptions<'bar'> = { + responsive: true, + scales: { + x: {}, + y: { beginAtZero: true } + } + }; + + constructor(private cdRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.renderChart(); + } + + ngAfterViewInit() { + setTimeout(() => { + if (this.chartData) { + this.renderChart(); + } + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['chartData'] && this.chartData) { + this.renderChart(); + } + } + + previousState(): void { + window.history.back(); + } + + private renderChart(): void { + // If a DashboardComponent was given, just build + if (this.dashboardComponent) { + this.dashboardComponentId = this.dashboardComponent?.id; + this.title = this.dashboardComponent?.name ?? ""; + //TODO : Build + this.build(); + } else if (this.dashboardComponentId) { + // Must load the DashboardComponent first, and then build + this.build(); + } else { + // Unable to have a DashboardComponent. EXCEPTION + } + } + + private build(): void { + if (this.dashboardComponentId && this.periodVersionId) { + this.dashboardComponentService + .loadValuesForComponent(this.dashboardComponentId, this.periodVersionId) + .pipe( + map( + (res: HttpResponse) => { + const data = res.body ?? {}; + + // Check if the data is already a Map or if it's a plain object + if (data instanceof Map) { + return data; + } else { + // Convert object to Map if it's not. + // I had to do it like this, otherwise, it wouldn't be a Map(). + const map = new Map(); + Object.keys(data).forEach(key => { + map.set(key, data[key]); + }); + return map; + } + } + ) + ) + .subscribe( + (_dashboardComponentDetailValues: Map) => { + this.dashboardComponentDetailValues = _dashboardComponentDetailValues; + this.buildChart(); + } + ); + } + } + + private buildChart(): void { + this.barChartLabels.length = 0; // Clear the label array + + const variableMap: Map = new Map(); + const variableLabelValues: Map = new Map(); + const variableColorValues: Map = new Map(); + + this.dashboardComponentDetailValues.forEach((value, key) => { + console.log(`Key: ${key}`); + let isFirst = true; + + value.forEach(item => { + if (isFirst) this.barChartLabels.push(item?.organization?.code || 'undefined'); //Add company label. Get it from the first element. + isFirst = false; + + const variableValues = variableMap.get(item?.dashboardComponentDetail?.variable?.id.toString() || ''); + + if (variableValues !== undefined) { + // Append value to array + variableValues.push(item.value || 0); + } else { + // Insert first entry + variableMap.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', [item.value || 0]); + variableLabelValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.name || 'undefined'); + variableColorValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.color || ''); + } + }); + }); + + //Finally, create data for the chart. + const myChartData: ChartData<'bar'> = { + labels: [], + datasets: [ + { data: [], label: 'Dataset 1', backgroundColor: 'rgba(75,192,192,0.6)' } + ] + }; + + myChartData.labels = this.barChartLabels; + myChartData.datasets.length = 0; + + variableMap.forEach((value, key) => { + const dataset: { data: number[]; label: string; backgroundColor: string } = { + data: value, + label: variableLabelValues.get(key) || 'undefined', + backgroundColor: variableColorValues.get(key) || '', + }; + + myChartData.datasets.push(dataset); + }); + + this.chartData = myChartData; + } + + private loadDashboardComponent(_dashboardComponentId: number): void { + this.dashboardComponent = null; + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.css b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.css new file mode 100644 index 0000000..ec393b3 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.css @@ -0,0 +1,17 @@ +table th:first-child { + min-width: 300px; +} + +/* Align all columns except the first one to the right */ +td:not(:first-child), th:not(:first-child) { + text-align: center; +} + +.dashboard-component-widget hr { + margin-top: 0px; + margin-bottom: 3px; +} + +.dashboard-component-widget h3 { + margin-bottom: 0px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.html b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.html new file mode 100644 index 0000000..49ec281 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.html @@ -0,0 +1,20 @@ + {{ '' // dashboard-component-table }} +
+
+

{{ title }}

+
+ + + + + + + + + + + +
{{ col }}
{{ cell }}
+
+
+ \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.ts b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.ts new file mode 100644 index 0000000..343fb41 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-table.component.ts @@ -0,0 +1,163 @@ +/** + * DashboardComponent WIDGET + * + * DashboardComponent component widget to show component output data. Will draw the widget accordantly to the DashboardComponent configuration (TABLE or CHART) + + * Input's: + * - dashboardComponent : The DashboardComponent instance to draw. If present, dashboardComponentId input will be IGNORED. + * - dashboardComponentId: The DashboardComponent id. + * + * Event's: + * + * + * Usage + * - In the intended *.component.html insert the code: + * + * OR + * + * - In the *.component.ts insert the code: + * import { DashboardComponentWidgetComponent } from 'app/entities/dashboard-component/factory/widgets/dashboard-component-widget.component'; + * ... + * add component imports, @Component({ imports: [ DashboardComponentWidgetComponent ] }) + * ... + * _dashboardComponent= input(null); + * OR + * _dashboardComponentId= input(null); + */ + +import { Component, Output, Input, inject, OnInit, EventEmitter } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { map, tap, filter } from 'rxjs'; + +// CHART.JS +import SharedModule from 'app/shared/shared.module'; +import { IDashboardComponent } from '../../dashboard-component.model'; +import { IDashboardComponentDetailValue } from '../../dashboard-component-detail-value.model'; +import { DashboardComponentService } from '../../service/dashboard-component.service'; + +@Component({ + standalone: true, + selector: 'dashboard-component-table', + templateUrl: './dashboard-component-table.component.html', + styleUrl: './dashboard-component-table.component.css', + imports: [SharedModule, RouterModule] +}) +export class DashboardComponentTable implements OnInit { + @Input() dashboardComponent: IDashboardComponent | null = null; // DashboardComponent instance, if the consumer already loaded a DashboardComponent instance. + @Input() dashboardComponentId: number | null = null; // DashboardComponent Id, if the consumer doesn't have an instance + @Input() periodVersionId: number | null = null; // PeriodVersion Id, needed to retrive the Output data for the component + + dashboardComponentDetailValues: Map = new Map(); + + columns: string[] = []; + rows: any[] = []; + title: string = ""; + + protected dashboardComponentService = inject(DashboardComponentService); + + ngOnInit(): void { + // If a DashboardComponent was given, just build + if (this.dashboardComponent) { + this.dashboardComponentId = this.dashboardComponent?.id; + this.title = this.dashboardComponent?.name ?? ""; + //TODO : Build + this.build(); + } else if (this.dashboardComponentId) { + // Must load the DashboardComponent first, and then build + this.build(); + } else { + // Unable to have a DashboardComponent. EXCEPTION + } + } + + previousState(): void { + window.history.back(); + } + + private build(): void { + if (this.dashboardComponentId && this.periodVersionId) { + this.dashboardComponentService + .loadValuesForComponent(this.dashboardComponentId, this.periodVersionId) + .pipe( + map( + (res: HttpResponse) => { + const data = res.body ?? {}; + + // Check if the data is already a Map or if it's a plain object + if (data instanceof Map) { + return data; + } else { + // Convert object to Map if it's not. + // I had to do it like this, otherwise, it wouldn't be a Map(). + const map = new Map(); + Object.keys(data).forEach(key => { + map.set(key, data[key]); + }); + return map; + } + } + ) + ) + .subscribe( + (_dashboardComponentDetailValues: Map) => { + this.dashboardComponentDetailValues = _dashboardComponentDetailValues; + this.buildTable(); + } + ); + } + } + + private buildTable(): void { + // Clear previous table + this.columns.length = 0; + this.rows.length = 0; + + const variableMap: Map = new Map(); + const variableLabelValues: Map = new Map(); + const variableColorValues: Map = new Map(); + + // First column header. Variable description column + this.columns.push(""); + + // Rebuild + this.dashboardComponentDetailValues.forEach((value, key) => { + let isFirst = true; + + value.forEach(item => { + if (isFirst) this.columns.push(item?.organization?.code || 'undefined'); //Add company label. Get it from the first element. + isFirst = false; + + const variableValues = variableMap.get(item?.dashboardComponentDetail?.variable?.id.toString() || ''); + + if (variableValues !== undefined) { + // Append value to array + variableValues.push(item.value || 0); + } else { + // Insert first entry + variableMap.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', [item.value || 0]); + variableLabelValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.name || 'undefined'); + variableColorValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.color || ''); + } + }); + }); + + // Build row array (Label | Value 1 | Value 2 | .. ) + variableMap.forEach((value, key) => { + let newRow : any[] = []; + + newRow.push(variableLabelValues.get(key)); + value.forEach(val => { + let roundedNumber = parseFloat(val.toFixed(2)); + newRow.push(roundedNumber); + }); + + this.rows.push(newRow); + }); + + } + + private loadDashboardComponent(_dashboardComponentId: number): void { + this.dashboardComponent = null; + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.css b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.css new file mode 100644 index 0000000..edaac33 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.css @@ -0,0 +1,44 @@ +::ng-deep .mat-expansion-panel-header { + height: auto !important; +} + +::ng-deep .mat-expansion-panel-body { + padding: 0px !important; +} + +.mat-content { + display: flex; /* Enables horizontal layout */ + width: 100%; /* Ensures it takes the full parent width */ +} + +.column { + padding: 2px 2px; + text-align: center; + /* border: 1px solid blue; */ +} + +.label { + width: 400px; + text-align: left; +} + +.values { + width: 90px; + text-align: right; +} + +/* Disable default styling for disabled elements */ +.make-disabled-enabled mat-expansion-panel-header { + background-color: inherit; /* Remove default background-color */ + color: inherit; /* Keep the text color */ + opacity: 1; /* Remove the default opacity applied when disabled */ + pointer-events: none; +} + +.accordion-header .column { + font-weight: bold; +} + +.tooltip-force-show { + pointer-events: auto !important; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html new file mode 100644 index 0000000..e1a3674 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html @@ -0,0 +1,21 @@ +
Loading...
+ + + + +
+
{{ label }}
+
+
+ + +
{{ item.label }}
+
{{ value }}
+
+ +
Loading...
+ + + +
+
diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html.html.solution2 b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html.html.solution2 new file mode 100644 index 0000000..09699ef --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html.html.solution2 @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID {{ element.id }} Name {{ element.name }} Value {{ element.value }} Actions + +
+ olá mundo + +
diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html.ts.solution2 b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html.ts.solution2 new file mode 100644 index 0000000..551b140 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.html.ts.solution2 @@ -0,0 +1,49 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatTableModule } from '@angular/material/table'; +import { MatIconModule } from '@angular/material/icon'; + +interface DataItem { + id: number; + name: string; + value: string; + details?: DataItem[]; +} + +@Component({ + standalone: true, + selector: 'app-nested-accordion', + templateUrl: './nested-accordion.component.html', + /* styleUrls: ['./nested-accordion.component.css'], */ + imports: [CommonModule, MatExpansionModule, MatTableModule, MatIconModule] +}) +export class NestedAccordionComponent { + displayedColumns: string[] = ['id', 'name', 'value']; + dataSource: DataItem[] = [ + { id: 1, name: 'Item 1', value: 'Value 1' }, + { id: 2, name: 'Item 2', value: 'Value 2' }, + { id: 3, name: 'Item 3', value: 'Value 3' } + ]; + + expandedRow: number | null = null; + expandedDetails: { [key: number]: DataItem[] } = {}; + + getDetails(rowId: number) { + if (!this.expandedDetails[rowId]) { + setTimeout(() => { // Simulate lazy loading + this.expandedDetails[rowId] = [ + { id: rowId * 10 + 1, name: 'Detail 1', value: 'Detail Value 1' }, + { id: rowId * 10 + 2, name: 'Detail 2', value: 'Detail Value 2' } + ]; + }, 500); // Simulated delay + } + } + + toggleRow(row: DataItem) { + this.expandedRow = this.expandedRow === row.id ? null : row.id; + if (this.expandedRow !== null) { + this.getDetails(row.id); + } + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.ts b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.ts new file mode 100644 index 0000000..ee27025 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/nested-accordion.component.ts @@ -0,0 +1,195 @@ +import { Component, Input, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { map, tap, filter, Subscription } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; + +import { DashboardComponentService } from '../../service/dashboard-component.service'; +import { IDashboardComponentDetailValue } from '../../dashboard-component-detail-value.model'; +import { ResilientEnvironmentService } from 'app/resilient/resilient-environment/service/resilient-environment.service'; +import { FormatHelper } from 'app/shared/utils/format-helper'; + +export interface AccordionItem { + id: string; + label: string; + help?: string; + values: string[]; + target?: number; + loading: boolean; + expanded: boolean; + children?: AccordionItem[]; // Nested children +} + +@Component({ + standalone: true, + selector: 'app-nested-accordion', + templateUrl: './nested-accordion.component.html', + styleUrls: ['./nested-accordion.component.css'], + imports: [CommonModule, MatExpansionModule, MatTableModule, MatTooltipModule] +}) +export class NestedAccordionComponent implements OnInit { + @Input() data: AccordionItem[] = []; // Input data (array) + @Input() labels: string[] = []; // Input labels (array) + @Input() level: number = 0; // Current level for styling + @Input() periodVersionId: number | null = null; + + loading = true; + + subscriptionWorkOrganization!: Subscription; // Store the subscription + selectedOrganizationId?: number; + displayedColumns: string[] = ['name', 'age', 'address', 'expand']; + + protected dashboardComponentService = inject(DashboardComponentService); + protected envService = inject(ResilientEnvironmentService); + + ngOnInit(): void { + // Get the currently selected ORGANIZATION, by ResilientEnvironmentService + this.subscriptionWorkOrganization = this + .envService + .selectedWorkOrganization + /* .pipe(first()) // READS the Observable only once. Doesn't react to later changes on the subscription */ + .subscribe(org => { + if(org){ + // Set selected + this.selectedOrganizationId = org.id; + + } + }); + + this.loading = false; + } + + // Simulates loading child data + loadChildren(item: AccordionItem) { + if (!item.children && item.target) { + item.loading = true; // Show loading + + //Invoke + this.loadValues(item, item.target); + } + } + + panelOpened(row: AccordionItem) { + this.loadChildren(row); + row.expanded = true; + } + + panelClosed(row: AccordionItem) { + row.expanded = false; + } + + private loadValues(item: AccordionItem, dashboardComponentId: number): void { + if (dashboardComponentId && this.periodVersionId && this.selectedOrganizationId) { + this.dashboardComponentService + .loadOrganizationValuesForComponent(this.selectedOrganizationId, dashboardComponentId, this.periodVersionId) + .pipe( + map( + (res: HttpResponse) => { + const data = res.body ?? {}; + + // Check if the data is already a Map or if it's a plain object + if (data instanceof Map) { + return data; + } else { + // Convert object to Map if it's not. + // I had to do it like this, otherwise, it wouldn't be a Map(). + const map = new Map(); + Object.keys(data).forEach(key => { + map.set(key, data[key]); + }); + + const reorderedMap = this.moveTopOrgToLast(map); + + return reorderedMap; + } + } + ) + ) + .subscribe( + (_dashboardComponentDetailValues: Map) => { + this.buildAccordion(item, _dashboardComponentDetailValues); + } + ); + } + } + + private moveTopOrgToLast(map: Map): Map { + if (!map.has("1")) return map; // If key "9" doesn't exist, return map as is + + const entries = Array.from(map.entries()); // Convert Map to array of entries + const entryTopOrg = entries.find(([key]) => key === "1"); // Find the entry with key "1" + const filteredEntries = entries.filter(([key]) => key !== "1"); // Remove "9" entry + + if (entryTopOrg) { + filteredEntries.push(entryTopOrg); // Move it to the end + } + + return new Map(filteredEntries); // Create a new Map with updated order + } + + private buildAccordion(item: AccordionItem, dashboardComponentDetailValues: Map ): void { + // Clear previous table + //this.labels.length = 0; + //this.data.length = 0; + + const variableMap: Map = new Map(); + const variableLabelValues: Map = new Map(); + const variableTargetValues: Map = new Map(); + const variableColorValues: Map = new Map(); + const variableHelpValues: Map = new Map(); + + // First column header. Variable description column + //this.labels.push(""); + + // Rebuild + dashboardComponentDetailValues.forEach((value, key) => { + let isFirst = true; + + value.forEach(item => { + //if (isFirst) this.labels.push(item?.organization?.code || 'undefined'); //Add company label. Get it from the first element. + //isFirst = false; + + const variableValues = variableMap.get(item?.dashboardComponentDetail?.variable?.id.toString() || ''); + + if (variableValues !== undefined) { + // Append value to array + variableValues.push(item.value || 0); + } else { + // Insert first entry + variableMap.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', [item.value || 0]); + variableLabelValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.name || 'undefined'); + variableColorValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.color || ''); + variableHelpValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.dashboardComponentDetail?.help || ''); + variableTargetValues.set(item?.dashboardComponentDetail?.variable?.id.toString() || '', item?.targetDashboardComponent?.id || null); + } + }); + }); + + // Build row array (Label | Value 1 | Value 2 | .. ) + item.children = []; + variableMap.forEach((value, key) => { + let newDataRow : AccordionItem = { + id: key, + label: variableLabelValues.get(key) || "", + help: variableHelpValues.get(key) || "", + values: [], + target: variableTargetValues.get(key) || undefined, + loading: false, + expanded: false, + children: undefined, + }; + + value.forEach(val => { + let roundedNumber = FormatHelper.formatDecimalValue(val); + + newDataRow.values.push(roundedNumber); + }); + + if (item.children) item.children.push(newDataRow); + }); + + item.loading = false; + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.html b/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.html new file mode 100644 index 0000000..9c05c38 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.html @@ -0,0 +1,118 @@ + +
+

+ Dashboard Components + +
+ + + +
+

+ + + + + + @if (dashboardComponents && dashboardComponents.length >= 0) { +
+ + + + + + + + + + + + @for (dashboardComponent of dashboardComponents; track trackId) { + + + + + + + + } + +
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
{{ dashboardComponent.code }}{{ dashboardComponent.name }}{{ dashboardComponent.description }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } + + @if (dashboardComponents?.length === 0) { +
+ Nenhum Dashboard Component encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.spec.ts b/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.spec.ts new file mode 100644 index 0000000..77cfb8e --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../dashboard-component.test-samples'; +import { DashboardComponentService } from '../service/dashboard-component.service'; + +import { DashboardComponentComponent } from './dashboard-component.component'; +import SpyInstance = jest.SpyInstance; + +describe('DashboardComponent Management Component', () => { + let comp: DashboardComponentComponent; + let fixture: ComponentFixture; + let service: DashboardComponentService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, DashboardComponentComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(DashboardComponentComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(DashboardComponentComponent); + comp = fixture.componentInstance; + service = TestBed.inject(DashboardComponentService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.variableClassTypes?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to variableClassTypeService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getDashboardComponentIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getDashboardComponentIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.ts b/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.ts new file mode 100644 index 0000000..68757e5 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/list/dashboard-component.component.ts @@ -0,0 +1,124 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IDashboardComponent, NewDashboardComponent } from '../dashboard-component.model'; +import { EntityArrayResponseType, DashboardComponentService } from '../service/dashboard-component.service'; +import { DashboardComponentDeleteDialogComponent } from '../delete/dashboard-component-delete-dialog.component'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +@Component({ + standalone: true, + selector: 'jhi-dashboard-component', + templateUrl: './dashboard-component.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class DashboardComponentComponent extends AbstractResilientEntityComponent implements OnInit { + subscription: Subscription | null = null; + dashboardComponents?: IDashboardComponent[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected dashboardComponentService = inject(DashboardComponentService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IDashboardComponent): number => this.dashboardComponentService.getEntityIdentifier(item); + + constructor(service: DashboardComponentService) { super(service); } + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.dashboardComponents || this.dashboardComponents.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(dashboardComponent: IDashboardComponent): void { + const modalRef = this.modalService.open(DashboardComponentDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.dashboardComponent = dashboardComponent; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.dashboardComponents = this.refineData(dataFromBody); + } + + protected refineData(data: IDashboardComponent[]): IDashboardComponent[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IDashboardComponent[] | null): IDashboardComponent[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.dashboardComponentService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/preview/dashboard-component-preview.component.html b/src/main/webapp/app/entities/dashboard-component/preview/dashboard-component-preview.component.html new file mode 100644 index 0000000..3fcd048 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/preview/dashboard-component-preview.component.html @@ -0,0 +1,93 @@ + +
+
+ @if (dashboardComponent()) { +
+

Dashboard Component

+ +
+ + + + + +
+
Code
+
+ {{ dashboardComponent()!.code }} +
+
Name
+
+ {{ dashboardComponent()!.name }} +
+
Description
+
+ {{ dashboardComponent()!.description }} +
+
Is Active ?
+
+ {{ dashboardComponent()!.isActive }} +
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + +
NameOutput
{{i+1}} + {{ dashboardComponentDetail.name }} + + {{dashboardComponentDetail!.variable!.name}} +
+
+
+ + + + + + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/dashboard-component/preview/dashboard-component-preview.component.ts b/src/main/webapp/app/entities/dashboard-component/preview/dashboard-component-preview.component.ts new file mode 100644 index 0000000..43f6fd4 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/preview/dashboard-component-preview.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectorRef, Component, input, OnInit, AfterViewInit, inject, ViewChild, ViewContainerRef, ComponentFactoryResolver } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { map } from 'rxjs'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IDashboardComponent } from '../dashboard-component.model'; +import { DashboardComponentService } from '../service/dashboard-component.service'; +import { IDashboardComponentDetail } from '../dashboard-component-detail.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { DashboardComponentChart } from 'app/entities/dashboard-component/factory/widgets/dashboard-component-chart.component'; +import { DashboardComponentFactoryService } from '../factory/dashboard-component-factory.service'; + +@Component({ + standalone: true, + selector: 'jhi-dashboard-component-preview', + templateUrl: './dashboard-component-preview.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe, DashboardComponentChart], +}) +export class DashboardComponentPreviewComponent implements OnInit, AfterViewInit { + @ViewChild('dashboardContainer', { read: ViewContainerRef }) container!: ViewContainerRef; + + dashboardComponent = input(null); + currentTab: string = 'details'; // default tab + + protected dashboardComponentService = inject(DashboardComponentService); + + constructor( + private dashboardComponentFactory: DashboardComponentFactoryService, + private cdRef: ChangeDetectorRef // Inject ChangeDetectorRef + ) {} + + ngOnInit(): void { + + } + + ngAfterViewInit() { + this.loadDashboard(); + } + + setTab(tabId: string): void { + this.currentTab = tabId; + } + + previousState(): void { + window.history.back(); + } + + loadDashboard() { + const _dashboardComponent: IDashboardComponent | null = this.dashboardComponent(); + if (_dashboardComponent?.type) { + const data = { dashboardComponent: _dashboardComponent }; + this.container.clear(); + this.dashboardComponentFactory.createComponent(_dashboardComponent?.type, this.container, data); + + // **Force Angular to detect changes** + this.cdRef.detectChanges(); + } + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/route/dashboard-component-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/dashboard-component/route/dashboard-component-routing-resolve.service.spec.ts new file mode 100644 index 0000000..ca76c62 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/route/dashboard-component-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IDashboardComponent } from '../dashboard-component.model'; +import { DashboardComponentService } from '../service/dashboard-component.service'; + +import dashboardComponentResolve from './dashboard-component-routing-resolve.service'; + +describe('DashboardComponent routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: DashboardComponentService; + let resultDashboardComponent: IDashboardComponent | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(DashboardComponentService); + resultDashboardComponent = undefined; + }); + + describe('resolve', () => { + it('should return IDashboardComponent returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + dashboardComponentResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultDashboardComponent = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultDashboardComponent).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + dashboardComponentResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultDashboardComponent = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultDashboardComponent).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + dashboardComponentResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultDashboardComponent = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultDashboardComponent).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/dashboard-component/route/dashboard-component-routing-resolve.service.ts b/src/main/webapp/app/entities/dashboard-component/route/dashboard-component-routing-resolve.service.ts new file mode 100644 index 0000000..5b0a3e9 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/route/dashboard-component-routing-resolve.service.ts @@ -0,0 +1,85 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { AlertService } from 'app/core/util/alert.service'; + +import { IDashboardComponent } from '../dashboard-component.model'; +import { DashboardComponentService } from '../service/dashboard-component.service'; +import { SecurityAction} from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; + +const dashboardComponentResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + const dashboardComponentService = inject(DashboardComponentService); + const router = inject(Router); + const translate = inject(TranslateService); + const alertService = inject(AlertService); + + if (id) { + return inject(DashboardComponentService) + .find(id) + .pipe( + mergeMap((dashboardComponent: HttpResponse) => { + if (dashboardComponent.body) { + let canAccess: boolean = false; + let alertMessage: string = ""; + const entityDomain = dashboardComponent.body; + + const actionToken: SecurityAction | null = route.data['action']; + const resourceToken: Resources | null = route.data['resource']; + if (actionToken && resourceToken) { + switch (actionToken) { + case SecurityAction.CREATE: + canAccess = dashboardComponentService.canCreate(); + alertMessage = translate.instant('error.createNotAllowedForState'); + break; + case SecurityAction.READ: + canAccess = dashboardComponentService.canRead(entityDomain); + alertMessage = translate.instant('error.readNotAllowedForState'); + break; + case SecurityAction.UPDATE: + canAccess = dashboardComponentService.canUpdate(entityDomain); + alertMessage = translate.instant('error.updateNotAllowedForState'); + break; + case SecurityAction.DELETE: + canAccess = dashboardComponentService.canDelete(entityDomain); + alertMessage = translate.instant('error.deleteNotAllowedForState'); + break; + } + } + + if (canAccess) { + const ut = of(dashboardComponent.body); + return of(dashboardComponent.body); + } else { + // Entity state doesn't allow the action + // Show alert message to user + alertService.addAlert({ + type: 'danger' , // 'success' | 'danger' | 'warning' | 'info' + message: alertMessage, + timeout: 5000, // Optional: auto-dismiss after 5 seconds + }); + + // Redirect to list page + router.navigate(['/dashboard-component'], { + state: { message: alertMessage } + }); + + return EMPTY; + } + + + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default dashboardComponentResolve; diff --git a/src/main/webapp/app/entities/dashboard-component/service/dashboard-component.service.spec.ts b/src/main/webapp/app/entities/dashboard-component/service/dashboard-component.service.spec.ts new file mode 100644 index 0000000..20f2366 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/service/dashboard-component.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IDashboardComponent } from '../dashboard-component.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../dashboard-component.test-samples'; + +import { DashboardComponentService } from './dashboard-component.service'; + +const requireRestSample: IDashboardComponent = { + ...sampleWithRequiredData, +}; + +describe('DashboardComponent Service', () => { + let service: DashboardComponentService; + let httpMock: HttpTestingController; + let expectedResult: IDashboardComponent | IDashboardComponent[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(DashboardComponentService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a DashboardComponent', () => { + const dashboardComponent = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(dashboardComponent).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a DashboardComponent', () => { + const dashboardComponent = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(dashboardComponent).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a DashboardComponent', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of DashboardComponent', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a DashboardComponent', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addDashboardComponentToCollectionIfMissing', () => { + it('should add a DashboardComponent to an empty array', () => { + const dashboardComponent: IDashboardComponent = sampleWithRequiredData; + expectedResult = service.addDashboardComponentToCollectionIfMissing([], dashboardComponent); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(dashboardComponent); + }); + + it('should not add a DashboardComponent to an array that contains it', () => { + const dashboardComponent: IDashboardComponent = sampleWithRequiredData; + const dashboardComponentCollection: IDashboardComponent[] = [ + { + ...dashboardComponent, + }, + sampleWithPartialData, + ]; + expectedResult = service.addDashboardComponentToCollectionIfMissing(dashboardComponentCollection, dashboardComponent); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a DashboardComponent to an array that doesn't contain it", () => { + const dashboardComponent: IDashboardComponent = sampleWithRequiredData; + const dashboardComponentCollection: IDashboardComponent[] = [sampleWithPartialData]; + expectedResult = service.addDashboardComponentToCollectionIfMissing(dashboardComponentCollection, dashboardComponent); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(dashboardComponent); + }); + + it('should add only unique DashboardComponent to an array', () => { + const dashboardComponentArray: IDashboardComponent[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const dashboardComponentCollection: IDashboardComponent[] = [sampleWithRequiredData]; + expectedResult = service.addDashboardComponentToCollectionIfMissing(dashboardComponentCollection, ...dashboardComponentArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const dashboardComponent: IDashboardComponent = sampleWithRequiredData; + const dashboardComponent2: IDashboardComponent = sampleWithPartialData; + expectedResult = service.addDashboardComponentToCollectionIfMissing([], dashboardComponent, dashboardComponent2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(dashboardComponent); + expect(expectedResult).toContain(dashboardComponent2); + }); + + it('should accept null and undefined values', () => { + const dashboardComponent: IDashboardComponent = sampleWithRequiredData; + expectedResult = service.addDashboardComponentToCollectionIfMissing([], null, dashboardComponent, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(dashboardComponent); + }); + + it('should return initial array if no DashboardComponent is added', () => { + const dashboardComponentCollection: IDashboardComponent[] = [sampleWithRequiredData]; + expectedResult = service.addDashboardComponentToCollectionIfMissing(dashboardComponentCollection, undefined, null); + expect(expectedResult).toEqual(dashboardComponentCollection); + }); + }); + + describe('compareDashboardComponent', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareDashboardComponent(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareDashboardComponent(entity1, entity2); + const compareResult2 = service.compareDashboardComponent(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareDashboardComponent(entity1, entity2); + const compareResult2 = service.compareDashboardComponent(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareDashboardComponent(entity1, entity2); + const compareResult2 = service.compareDashboardComponent(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/dashboard-component/service/dashboard-component.service.ts b/src/main/webapp/app/entities/dashboard-component/service/dashboard-component.service.ts new file mode 100644 index 0000000..9f77a88 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/service/dashboard-component.service.ts @@ -0,0 +1,56 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IDashboardComponent, NewDashboardComponent } from '../dashboard-component.model'; +import { IDashboardComponentDetail } from '../dashboard-component-detail.model'; +import { IDashboardComponentDetailValue } from '../dashboard-component-detail-value.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; + +export type PartialUpdateDashboardComponent = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class DashboardComponentService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/dashboard-components'); + getResourceUrl(): string { return this.resourceUrl; } + + build(id: number): Observable>> { + // No convertion is needed + return this.http.get>(`${this.getResourceUrl()}/build/${id}`, { observe: 'response' }); + } + + loadValuesForComponent(componentId: number, periodVersionId: number): Observable>> { + // No convertion is needed + return this.http.get>(`${this.getResourceUrl()}/build/${componentId}/${periodVersionId}`, { observe: 'response' }); + } + + loadOrganizationValuesForComponent(organizationId: number, componentId: number, periodVersionId: number): Observable>> { + // No convertion is needed + return this.http.get>(`${this.getResourceUrl()}/build/${organizationId}/${componentId}/${periodVersionId}`, { observe: 'response' }); + } + + getAllActive(): Observable { + // No convertion is needed + return this.http.get(`${this.getResourceUrl()}/active`, { observe: 'response' }); + } + + getAllActiveForView(dashboardView: string): Observable { + // No convertion is needed + return this.http.get(`${this.getResourceUrl()}/active/${dashboardView}`, { observe: 'response' }); + } + + export(organizationId: number, dashboardComponentId: number, periodVersionId: number): Observable { + return this.http.get(`${this.resourceUrl}/export/${organizationId}/${dashboardComponentId}/${periodVersionId}`, { responseType: 'blob' }); + } + +} diff --git a/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-detail-form.service.ts b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-detail-form.service.ts new file mode 100644 index 0000000..1f21a43 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-detail-form.service.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import { IDashboardComponentDetail, NewDashboardComponentDetail } from '../dashboard-component-detail.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IDashboardComponentDetail for edit and NewDashboardComponentDetailFormGroupInput for create. + */ +type DashboardComponentDetailFormGroupInput = IDashboardComponentDetail | PartialWithRequiredKeyOf; + +type DashboardComponentDetailFormDefaults = Pick; + +type DashboardComponentDetailFormGroupContent = { + id: FormControl; + name: FormControl; + color: FormControl; + help: FormControl; + indexOrder: FormControl; + dashboardComponent: FormControl; + targetDashboardComponent: FormControl; + variable: FormControl; + version: FormControl; +}; + +export type DashboardComponentDetailFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class DashboardComponentDetailFormService { + createDashboardComponentDetailFormGroup(dashboardComponentDetail: DashboardComponentDetailFormGroupInput = { id: null }): DashboardComponentDetailFormGroup { + const dashboardComponentDetailRawValue = { + ...this.getFormDefaults(), + ...dashboardComponentDetail, + }; + return new FormGroup({ + id: new FormControl( + { value: dashboardComponentDetailRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + name: new FormControl(dashboardComponentDetailRawValue.name, { + validators: [Validators.required], + }), + color: new FormControl(dashboardComponentDetailRawValue.color), + help: new FormControl(dashboardComponentDetailRawValue.help), + indexOrder: new FormControl(dashboardComponentDetailRawValue.indexOrder, { + validators: [Validators.required], + }), + version: new FormControl(dashboardComponentDetailRawValue.version), + variable: new FormControl(dashboardComponentDetailRawValue.variable), + dashboardComponent: new FormControl(dashboardComponentDetailRawValue.dashboardComponent), + targetDashboardComponent: new FormControl(dashboardComponentDetailRawValue.targetDashboardComponent), + }); + } + + getDashboardComponentDetail(form: DashboardComponentDetailFormGroup): IDashboardComponentDetail | NewDashboardComponentDetail { + return form.getRawValue() as IDashboardComponentDetail | NewDashboardComponentDetail; + } + + resetForm(form: DashboardComponentDetailFormGroup, dashboardComponentDetail: DashboardComponentDetailFormGroupInput): void { + const dashboardComponentDetailRawValue = { ...this.getFormDefaults(), ...dashboardComponentDetail }; + form.reset( + { + ...dashboardComponentDetailRawValue, + id: { value: dashboardComponentDetailRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): DashboardComponentDetailFormDefaults { + return { + id: null, + }; + } +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-form.service.spec.ts b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-form.service.spec.ts new file mode 100644 index 0000000..c61f216 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-form.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../dashboard-component.test-samples'; + +import { DashboardComponentFormService } from './dashboard-component-form.service'; + +describe('DashboardComponent Form Service', () => { + let service: DashboardComponentFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DashboardComponentFormService); + }); + + describe('Service methods', () => { + describe('createDashboardComponentFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createDashboardComponentFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + label: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + + it('passing IDashboardComponent should create a new form with FormGroup', () => { + const formGroup = service.createDashboardComponentFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + label: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + }); + + describe('getDashboardComponent', () => { + it('should return NewDashboardComponent for default DashboardComponent initial value', () => { + const formGroup = service.createDashboardComponentFormGroup(sampleWithNewData); + + const DashboardComponent = service.getDashboardComponent(formGroup) as any; + + expect(DashboardComponent).toMatchObject(sampleWithNewData); + }); + + it('should return NewDashboardComponent for empty DashboardComponent initial value', () => { + const formGroup = service.createDashboardComponentFormGroup(); + + const DashboardComponent = service.getDashboardComponent(formGroup) as any; + + expect(DashboardComponent).toMatchObject({}); + }); + + it('should return IDashboardComponent', () => { + const formGroup = service.createDashboardComponentFormGroup(sampleWithRequiredData); + + const DashboardComponent = service.getDashboardComponent(formGroup) as any; + + expect(DashboardComponent).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IDashboardComponent should not enable id FormControl', () => { + const formGroup = service.createDashboardComponentFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewDashboardComponent should disable id FormControl', () => { + const formGroup = service.createDashboardComponentFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-form.service.ts b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-form.service.ts new file mode 100644 index 0000000..1b6f8e7 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-form.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import { IDashboardComponent, NewDashboardComponent } from '../dashboard-component.model'; +import { DashboardComponentDetailFormGroup, DashboardComponentDetailFormService } from './dashboard-component-detail-form.service'; +import { DashboardComponentOrganizationFormGroup, DashboardComponentOrganizationFormService } from './dashboard-component-organization-form.service'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IDashboardComponent for edit and NewDashboardComponentFormGroupInput for create. + */ +type DashboardComponentFormGroupInput = IDashboardComponent | PartialWithRequiredKeyOf; + +type DashboardComponentFormDefaults = Pick; + +type DashboardComponentFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + type: FormControl; + view: FormControl; + chartType: FormControl; + description: FormControl; + isActive: FormControl; + isExportable: FormControl; + dashboardComponentDetails: FormArray; + dashboardComponentOrganizations: FormArray; + version: FormControl; +}; + +export type DashboardComponentFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class DashboardComponentFormService { + constructor(private fb: FormBuilder, + private dashboardComponentDetailFormService: DashboardComponentDetailFormService, + private dashboardComponentOrganizationFormService: DashboardComponentOrganizationFormService ) {} + + createDashboardComponentFormGroup(dashboardComponent: DashboardComponentFormGroupInput = { id: null }): DashboardComponentFormGroup { + const dashboardComponentRawValue = { + ...this.getFormDefaults(), + ...dashboardComponent, + }; + + const items = dashboardComponent?.dashboardComponentDetails??[]; + + return new FormGroup({ + id: new FormControl( + { value: dashboardComponentRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(dashboardComponentRawValue.code, { + validators: [Validators.required], + }), + name: new FormControl(dashboardComponentRawValue.name, { + validators: [Validators.required], + }), + type: new FormControl(dashboardComponentRawValue.type, { + validators: [Validators.required], + }), + view: new FormControl(dashboardComponentRawValue.view), + chartType: new FormControl(dashboardComponentRawValue.chartType), + description: new FormControl(dashboardComponentRawValue.description), + isActive: new FormControl(dashboardComponentRawValue.isActive), + isExportable: new FormControl(dashboardComponentRawValue.isExportable, { + validators: [Validators.required], + }), + dashboardComponentDetails: new FormArray(items.map(item => this.dashboardComponentDetailFormService.createDashboardComponentDetailFormGroup(item))), + dashboardComponentOrganizations: new FormArray(items.map(item => this.dashboardComponentOrganizationFormService.createDashboardComponentOrganizationFormGroup(item))), + version: new FormControl(dashboardComponentRawValue.version), + }); + } + + getDashboardComponent(form: DashboardComponentFormGroup): IDashboardComponent | NewDashboardComponent { + return form.getRawValue() as IDashboardComponent | NewDashboardComponent; + } + + resetForm(form: DashboardComponentFormGroup, dashboardComponent: DashboardComponentFormGroupInput): void { + const dashboardComponentRawValue = { + ...this.getFormDefaults(), + ...dashboardComponent + }; + form.reset( + { + ...dashboardComponentRawValue, + id: { value: dashboardComponentRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): DashboardComponentFormDefaults { + return { + id: null, + isExportable: false, + }; + } +} diff --git a/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-organization-form.service.ts b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-organization-form.service.ts new file mode 100644 index 0000000..d261684 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-organization-form.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import { IDashboardComponentDetail, NewDashboardComponentDetail } from '../dashboard-component-detail.model'; +import { IDashboardComponentOrganization, NewDashboardComponentOrganization } from '../dashboard-component-organization.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IDashboardComponentOrganization for edit and NewDashboardComponentOrganizationFormGroupInput for create. + */ +type DashboardComponentOrganizationFormGroupInput = IDashboardComponentOrganization | PartialWithRequiredKeyOf; + +type DashboardComponentOrganizationFormDefaults = Pick; + +type DashboardComponentOrganizationFormGroupContent = { + id: FormControl; + dashboardComponent: FormControl; + organization: FormControl; + version: FormControl; +}; + +export type DashboardComponentOrganizationFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class DashboardComponentOrganizationFormService { + createDashboardComponentOrganizationFormGroup(dashboardComponentOrganization: DashboardComponentOrganizationFormGroupInput = { id: null }): DashboardComponentOrganizationFormGroup { + const dashboardComponentOrganizationRawValue = { + ...this.getFormDefaults(), + ...dashboardComponentOrganization, + }; + return new FormGroup({ + id: new FormControl( + { value: dashboardComponentOrganizationRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + dashboardComponent: new FormControl(dashboardComponentOrganizationRawValue.dashboardComponent), + organization: new FormControl(dashboardComponentOrganizationRawValue.organization, { + validators: [Validators.required], + }), + version: new FormControl(dashboardComponentOrganizationRawValue.version), + }); + } + + getDashboardComponentOrganization(form: DashboardComponentOrganizationFormGroup): IDashboardComponentOrganization | NewDashboardComponentOrganization { + return form.getRawValue() as IDashboardComponentOrganization | NewDashboardComponentOrganization; + } + + resetForm(form: DashboardComponentOrganizationFormGroup, dashboardComponentOrganization: DashboardComponentOrganizationFormGroupInput): void { + const dashboardComponentOrganizationRawValue = { ...this.getFormDefaults(), ...dashboardComponentOrganization }; + form.reset( + { + ...dashboardComponentOrganizationRawValue, + id: { value: dashboardComponentOrganizationRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): DashboardComponentOrganizationFormDefaults { + return { + id: null, + }; + } +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.html b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.html new file mode 100644 index 0000000..79bdd04 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.html @@ -0,0 +1,344 @@ +
+
+
+

+ Criar ou editar Dashboard Component +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('type')!.invalid && (editForm.get('type')!.dirty || editForm.get('type')!.touched)) { +
+ @if (editForm.get('type')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+ +
+ + + @if (editForm.get('chartType')!.invalid && (editForm.get('chartType')!.dirty || editForm.get('chartType')!.touched)) { +
+ @if (editForm.get('chartType')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+
+ + @if (editForm.get('isActive')!.invalid && (editForm.get('isActive')!.dirty || editForm.get('isActive')!.touched)) { +
+ @if (editForm.get('isActive')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ +
+
+ + @if (editForm.get('isExportable')!.invalid && (editForm.get('isExportable')!.dirty || editForm.get('isExportable')!.touched)) { +
+ @if (editForm.get('isExportable')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
CodeOutputDetailColorHelp
+ + + + + {{ item.code }} - {{ item.name }} + + + {{ item.code }} - {{ item.name }} + + + + + + + + {{ '' // https://zefoy.github.io/ngx-color-picker/ }} + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + +
Organization
+ + + +
+
+
+ +
+ +
+ + + +
+
+
+
\ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.spec.ts b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.spec.ts new file mode 100644 index 0000000..4c2d487 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { DashboardComponentService } from '../service/dashboard-component.service'; +import { IDashboardComponent } from '../dashboard-component.model'; +import { DashboardComponentFormService } from './dashboard-component-form.service'; + +import { DashboardComponentUpdateComponent } from './dashboard-component-update.component'; + +describe('DashboardComponent Management Update Component', () => { + let comp: DashboardComponentUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let DashboardComponentFormService: DashboardComponentFormService; + let DashboardComponentService: DashboardComponentService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, DashboardComponentUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(DashboardComponentUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(DashboardComponentUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + DashboardComponentFormService = TestBed.inject(DashboardComponentFormService); + DashboardComponentService = TestBed.inject(DashboardComponentService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should update editForm', () => { + const DashboardComponent: IDashboardComponent = { id: 456 }; + + activatedRoute.data = of({ DashboardComponent }); + comp.ngOnInit(); + + expect(comp.DashboardComponent).toEqual(DashboardComponent); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const DashboardComponent = { id: 123 }; + jest.spyOn(DashboardComponentFormService, 'getDashboardComponent').mockReturnValue(DashboardComponent); + jest.spyOn(DashboardComponentService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ DashboardComponent }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: DashboardComponent })); + saveSubject.complete(); + + // THEN + expect(DashboardComponentFormService.getDashboardComponent).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(DashboardComponentService.update).toHaveBeenCalledWith(expect.objectContaining(DashboardComponent)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const DashboardComponent = { id: 123 }; + jest.spyOn(DashboardComponentFormService, 'getDashboardComponent').mockReturnValue({ id: null }); + jest.spyOn(DashboardComponentService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ DashboardComponent: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: DashboardComponent })); + saveSubject.complete(); + + // THEN + expect(DashboardComponentFormService.getDashboardComponent).toHaveBeenCalled(); + expect(DashboardComponentService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const DashboardComponent = { id: 123 }; + jest.spyOn(DashboardComponentService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ DashboardComponent }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(DashboardComponentService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.ts b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.ts new file mode 100644 index 0000000..606635f --- /dev/null +++ b/src/main/webapp/app/entities/dashboard-component/update/dashboard-component-update.component.ts @@ -0,0 +1,218 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule, FormArray } from '@angular/forms'; +import { DragDropModule, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { ColorPickerModule } from 'ngx-color-picker'; +import { NgSelectModule } from '@ng-select/ng-select'; + +import { IDashboardComponent, NewDashboardComponent } from '../dashboard-component.model'; +import { DashboardComponentType } from 'app/entities/enumerations/dashboard-component-type.model'; +import { DashboardComponentView } from 'app/entities/enumerations/dashboard-component-view.model'; +import { DashboardComponentChartType } from 'app/entities/enumerations/dashboard-component-chart-type.model'; +import { DashboardComponentService } from '../service/dashboard-component.service'; +import { DashboardComponentFormService, DashboardComponentFormGroup } from './dashboard-component-form.service'; +import { DashboardComponentDetailFormService } from './dashboard-component-detail-form.service'; +import { IVariable } from 'app/entities/variable/variable.model'; +import { VariableService } from 'app/entities/variable/service/variable.service'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; +import { DashboardComponentOrganizationFormService } from './dashboard-component-organization-form.service'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; + +@Component({ + standalone: true, + selector: 'jhi-dashboard-component-update', + templateUrl: './dashboard-component-update.component.html', + imports: [SharedModule, ColorPickerModule, DragDropModule, FormsModule, ReactiveFormsModule, NgSelectModule], +}) +export class DashboardComponentUpdateComponent extends AbstractResilientEntityComponent implements OnInit { + isSaving = false; + dashboardComponent: IDashboardComponent | null = null; + currentTab: string = 'details'; // default tab + dashboardComponentTypes = Object.keys(DashboardComponentType); + dashboardComponentViews = Object.keys(DashboardComponentView); + dashboardComponentChartTypes = Object.keys(DashboardComponentChartType); + + outputVariablesSharedCollection: IVariable[] = []; + organizationsSharedCollection: IOrganization[] = []; + dashboardComponentSharedCollection: IDashboardComponent[] = []; + + protected variableService = inject(VariableService); + protected organizationService = inject(OrganizationService); + protected dashboardComponentService = inject(DashboardComponentService); + protected dashboardComponentFormService = inject(DashboardComponentFormService); + protected dashboardComponentDetailFormService = inject(DashboardComponentDetailFormService); + protected dashboardComponentOrganizationFormService = inject(DashboardComponentOrganizationFormService); + protected activatedRoute = inject(ActivatedRoute); + + compareOutputVariable = (o1: IVariable | null, o2: IVariable | null): boolean => this.variableService.compareEntity(o1, o2); + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => this.organizationService.compareEntity(o1, o2); + compareDashboardComponent = (o1: IDashboardComponent | null, o2: IDashboardComponent | null): boolean => this.dashboardComponentService.compareEntity(o1, o2); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: DashboardComponentFormGroup = this.dashboardComponentFormService.createDashboardComponentFormGroup(); + + constructor(service: DashboardComponentService) { super(service); } + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ dashboardComponent }) => { + this.dashboardComponent = dashboardComponent; + if (dashboardComponent) { + this.updateForm(dashboardComponent); + } + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const dashboardComponent = this.dashboardComponentFormService.getDashboardComponent(this.editForm); + if (dashboardComponent.id !== null) { + this.subscribeToSaveResponse(this.dashboardComponentService.update(dashboardComponent)); + } else { + this.subscribeToSaveResponse(this.dashboardComponentService.create(dashboardComponent)); + } + } + + setTab(tabId: string): void { + this.currentTab = tabId; + } + + /* Nested collection methods : DashboardComponentDetail's */ + /* Use it like a property : this.dashboardComponentDetails */ + get dashboardComponentDetails(): FormArray { + return this.editForm.get('dashboardComponentDetails') as FormArray; + } + + addDashboardComponentDetail(): void { + // Calculate the indexOrder for the new DashboardComponentDetail + const maxIdx = this.dashboardComponentDetails.length ?? 0; + const newDashboardComponentDetail = this.dashboardComponentDetailFormService.createDashboardComponentDetailFormGroup(); + newDashboardComponentDetail.patchValue({indexOrder: maxIdx+1}); + + // Add to the formarray + this.dashboardComponentDetails.push( newDashboardComponentDetail ); + } + + removeDashboardComponentDetail(index: number): void { + this.dashboardComponentDetails.removeAt(index); + } + + /* Nested collection methods : DashboardComponentOrganization's */ + /* Use it like a property : this.dashboardComponentOrganizations */ + get dashboardComponentOrganizations(): FormArray { + return this.editForm.get('dashboardComponentOrganizations') as FormArray; + } + + addDashboardComponentOrganization(): void { + // Calculate the indexOrder for the new DashboardComponentOrganization + const maxIdx = this.dashboardComponentOrganizations.length ?? 0; + const newDashboardComponentOrganization = this.dashboardComponentOrganizationFormService.createDashboardComponentOrganizationFormGroup(); + + // Add to the formarray + this.dashboardComponentOrganizations.push( newDashboardComponentOrganization ); + } + + removeDashboardComponentOrganization(index: number): void { + this.dashboardComponentOrganizations.removeAt(index); + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(dashboardComponent: IDashboardComponent): void { + this.dashboardComponent = dashboardComponent; + this.dashboardComponentFormService.resetForm(this.editForm, dashboardComponent); + + // dashboardComponent.dashboardComponentDetails FormArray + dashboardComponent.dashboardComponentDetails?.forEach(dashCompDetail => { + this.dashboardComponentDetails.push(this.dashboardComponentDetailFormService.createDashboardComponentDetailFormGroup(dashCompDetail)); + }); + + // dashboardComponent.dashboardComponentOrganizations FormArray + dashboardComponent.dashboardComponentOrganizations?.forEach(dashCompOrg => { + this.dashboardComponentOrganizations.push(this.dashboardComponentOrganizationFormService.createDashboardComponentOrganizationFormGroup(dashCompOrg)); + }); + } + + protected loadRelationshipsOptions(): void { + const queryObject: any = { + eagerload: true, + //sort: this.sortService.buildSortParam(this.sortState()), + }; + var clause: string = "equals"; + var key: string = "output"; + var value: string = "true"; + queryObject[key + "." + clause] = value; + this.variableService + .query(queryObject) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((outputVariables: IVariable[]) => (this.outputVariablesSharedCollection = outputVariables)); + + var clause: string = "equals"; + var key: string = "type"; + var value: string = this.dashboardComponent?.type ?? ""; + queryObject[key + "." + clause] = value; + this.dashboardComponentService + .query(queryObject) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((dashboardComponents: IDashboardComponent[]) => (this.dashboardComponentSharedCollection = dashboardComponents)); + + this.organizationService + .queryByNature(OrganizationNature.ORGANIZATION) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((organizations: IOrganization[]) => (this.organizationsSharedCollection = organizations)); + + } + + drop(event: CdkDragDrop): void { + if (this.dashboardComponentDetails) { + // Save the original indexOrder values + const idx_1 = this.dashboardComponentDetails.at(event.previousIndex).value.indexOrder; + const idx_2 = this.dashboardComponentDetails.at(event.currentIndex).value.indexOrder; + + // Switch the indexOrder + this.dashboardComponentDetails?.at(event.previousIndex).patchValue({ indexOrder: idx_2 }); + this.dashboardComponentDetails?.at(event.currentIndex).patchValue({ indexOrder: idx_1 }); + + // Switch the values within the FormArray, to visually move the row + moveItemInArray(this.dashboardComponentDetails.controls, event.previousIndex, event.currentIndex); + } else { + // NEVER happens. To call drop(), the dashboardComponentDetails collections must have values + console.warn('dashboardComponent or dashboardComponentDetails is undefined'); + } + } + + variableSearchFn = (term: string, item: any) => { + term = term.toLowerCase(); + return (item.code + ' ' + item.name).toLowerCase().includes(term); + }; + +} diff --git a/src/main/webapp/app/entities/dashboard/dashboard.routes.ts b/src/main/webapp/app/entities/dashboard/dashboard.routes.ts new file mode 100644 index 0000000..676c2f0 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard/dashboard.routes.ts @@ -0,0 +1,27 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { DashboardPreviewComponent } from './preview/dashboard-preview.component'; +// import DashboardResolve from './route/dashboard-routing-resolve.service'; + +const dashboardRoute: Routes = [ + { + path: ':view/:period', + component: DashboardPreviewComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR', 'ROLE_USER'], + }, + }, + { + path: ':view/:period/:period_version', + component: DashboardPreviewComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR', 'ROLE_USER'], + }, + }, +]; + +export default dashboardRoute; diff --git a/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.html b/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.html new file mode 100644 index 0000000..53b2b59 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.html @@ -0,0 +1,18 @@ + +
+
+
+

{{ pageTitle }}

+

+
+ +
+ +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.ts b/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.ts new file mode 100644 index 0000000..56f9353 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.ts @@ -0,0 +1,169 @@ +import { ChangeDetectorRef, Component, input, OnInit, AfterViewInit, inject, ViewChild, ViewContainerRef, ComponentFactoryResolver } from '@angular/core'; +import { RouterModule, ActivatedRoute } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { map, tap, Observable, EMPTY } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { IDashboardComponent } from 'app/entities/dashboard-component/dashboard-component.model'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { DashboardComponentService, EntityArrayResponseType } from 'app/entities/dashboard-component/service/dashboard-component.service'; +import { DashboardComponentFactoryService } from 'app/entities/dashboard-component/factory/dashboard-component-factory.service'; +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodSelectorComponent } from 'app/resilient/resilient-environment/period-selector/period-selector.component'; + +@Component({ + standalone: true, + selector: 'jhi-dashboard-preview', + templateUrl: './dashboard-preview.component.html', + imports: [SharedModule, RouterModule, PeriodSelectorComponent], +}) +export class DashboardPreviewComponent implements OnInit, AfterViewInit { + @ViewChild('dashboardContainer', { read: ViewContainerRef }) container!: ViewContainerRef; + + dashboardComponents?: IDashboardComponent[]; + periodsSharedCollection: IPeriod[] = []; + + dashboardView: string | null = null; + periodId: number = 0; + periodVersionId: number | null = null; + isLoading = false; + + pageTitle: string = ''; + pageSubTitle: string = ''; + showComponentTitle: boolean = false; + + protected dashboardComponentService = inject(DashboardComponentService); + protected periodService = inject(PeriodService); + + constructor( + private dashboardComponentFactory: DashboardComponentFactoryService, + private cdRef: ChangeDetectorRef, // Inject ChangeDetectorRef + private route: ActivatedRoute + ) {} + + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => + this.periodService.compareEntity(o1, o2); + + ngOnInit(): void { + this.isLoading = true; + + this.route.paramMap.subscribe(params => { + this.dashboardView = params.get('view') ?? null; + + // used the operator (+) that auto-casts a string to a number + this.periodId = +(params.get('period') ?? 0); + + // periodVersion might not be defined. In this case, it will calculate the most recent version + if (params.get('period_version') != null) { + this.periodVersionId = +(params.get('period_version')!); + } + + // Page titles + this.setPageTitles(this.dashboardView); + + // Force build + this.build(); + }); + + // Get the latest PeriodVersion for the requested Period + if (this.periodVersionId == null) { + this.periodService + .lastVersion(this.periodId) + .subscribe({ + next: (version: HttpResponse) => { + if (version.body) { + const periodVersion = version.body; + this.periodVersionId = periodVersion.id; + } else { + this.periodVersionId = null; + } + + //Fire Loading of the dashboard + this.build(); + }, + }); + } else { + //Fire Loading of the dashboard + this.build(); + } + + this.loadRelationshipsOptions(); + } + + onSearch(): void { + this.build(); + } + + ngAfterViewInit() { + this.loadDashboards(); + } + + build(): void { + if (this.periodVersionId == null) { + // Can't load dashboard without a version + this.dashboardComponents = []; // Clear components + return; + } + + // Load a list of dashboarComponent's to render + this.loadDashboards().subscribe({ + next: (res: EntityArrayResponseType) => { + this.dashboardComponents = res.body ?? []; + + this.container.clear(); + + this.dashboardComponents.forEach((dashComp, index) => { + this.loadDashboard(dashComp, this.periodVersionId!); + }); + }, + }); + } + + loadDashboard(_dashboardComponent: IDashboardComponent, periodVersionId: number): void { + + if (_dashboardComponent?.type) { + const data = { dashboardComponent: _dashboardComponent, periodVersionId: this.periodVersionId, showTitle: this.showComponentTitle }; + this.dashboardComponentFactory.createComponent(_dashboardComponent?.type, this.container, data); + + // **Force Angular to detect changes** + this.cdRef.detectChanges(); + } + } + + protected loadDashboards(): Observable { + if (this.dashboardView) { + return this.dashboardComponentService.getAllActiveForView(this.dashboardView).pipe(tap(() => (this.isLoading = false))); + } else { + return EMPTY; + } + } + + protected loadRelationshipsOptions(): void { + this.periodService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((periods: IPeriod[]) => { + this.periodsSharedCollection = periods; + }); + } + + protected setPageTitles(view: string | null): void { + this.pageTitle = ''; + this.pageSubTitle = ''; + + if (view) { + if (view==='EMISSIONS') { + this.pageTitle = 'Emissões de Gases com Efeito de Estufa'; + this.pageSubTitle = 't CO2e'; + this.showComponentTitle = false; + } else if (view==='INDICATORS') { + this.pageTitle = 'Indicadores'; + this.pageSubTitle = ''; + this.showComponentTitle = true; + } + } + } +} diff --git a/src/main/webapp/app/entities/dashboard/route/dashboard-routing-resolve.service.ts b/src/main/webapp/app/entities/dashboard/route/dashboard-routing-resolve.service.ts new file mode 100644 index 0000000..f7faba6 --- /dev/null +++ b/src/main/webapp/app/entities/dashboard/route/dashboard-routing-resolve.service.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IDashboard } from '../dashboard-component.model'; +import { DashboardComponentService } from '../service/dashboard-component.service'; + +const dashboardComponentResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(DashboardComponentService) + .find(id) + .pipe( + mergeMap((dashboardComponent: HttpResponse) => { + if (dashboardComponent.body) { + const ut = of(dashboardComponent.body); + return of(dashboardComponent.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default dashboardComponentResolve; diff --git a/src/main/webapp/app/entities/document/delete/document-delete-dialog.component.html b/src/main/webapp/app/entities/document/delete/document-delete-dialog.component.html new file mode 100644 index 0000000..2ac7196 --- /dev/null +++ b/src/main/webapp/app/entities/document/delete/document-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (document) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/document/delete/document-delete-dialog.component.ts b/src/main/webapp/app/entities/document/delete/document-delete-dialog.component.ts new file mode 100644 index 0000000..9a331d5 --- /dev/null +++ b/src/main/webapp/app/entities/document/delete/document-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IDocument } from '../document.model'; +import { DocumentService } from '../service/document.service'; + +@Component({ + standalone: true, + templateUrl: './document-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class DocumentDeleteDialogComponent { + document?: IDocument; + + protected documentService = inject(DocumentService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.documentService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/document/detail/document-detail.component.html b/src/main/webapp/app/entities/document/detail/document-detail.component.html new file mode 100644 index 0000000..000fb05 --- /dev/null +++ b/src/main/webapp/app/entities/document/detail/document-detail.component.html @@ -0,0 +1,69 @@ +
+
+ @if (document()) { +
+

+ Document +

+ +
+ + + + + +
+
+ Title +
+
+ {{ document()!.title }} +
+
+ Description +
+
+ {{ document()!.description }} +
+
+ Data File +
+
+ @if (document()!.dataFile) { +
+ Abrir + {{ document()!.dataFileContentType }}, {{ byteSize(document()!.dataFile ?? '') }} +
+ } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/document/detail/document-detail.component.ts b/src/main/webapp/app/entities/document/detail/document-detail.component.ts new file mode 100644 index 0000000..e7594e0 --- /dev/null +++ b/src/main/webapp/app/entities/document/detail/document-detail.component.ts @@ -0,0 +1,57 @@ +import { Component, inject, input, OnInit } from '@angular/core'; +import { RouterModule, Router, ActivatedRoute } from '@angular/router'; + +import { AlertService } from 'app/core/util/alert.service'; +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { IDocument, NewDocument } from '../document.model'; +import { DocumentService } from '../service/document.service'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +@Component({ + standalone: true, + selector: 'jhi-document-detail', + templateUrl: './document-detail.component.html', + imports: [ + SharedModule, + RouterModule, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe + ], +}) +export class DocumentDetailComponent extends AbstractResilientEntityComponent implements OnInit { + document = input(null); + + protected dataUtils = inject(DataUtils); + protected documentService = inject(DocumentService); + protected alertService = inject(AlertService); + protected router = inject(Router); + protected route = inject(ActivatedRoute); + + constructor(service: DocumentService) { super(service); } + + ngOnInit(): void { + // Checks for messages in navigation state and shows the alert + const navState = window.history.state; + if (navState && navState.message) { + this.alertService.addAlert({ + type: 'success', + message: navState.message, + }); + } + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/document/document.model.ts b/src/main/webapp/app/entities/document/document.model.ts new file mode 100644 index 0000000..59ebb93 --- /dev/null +++ b/src/main/webapp/app/entities/document/document.model.ts @@ -0,0 +1,12 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +export interface IDocument extends IResilientEntity { + title?: string | null; + description?: string | null; + dataFile?: string | null; + dataFileContentType?: string | null; + uploadFileName?: string | null; + diskFileName?: string | null; +} + +export type NewDocument = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/document/document.routes.ts b/src/main/webapp/app/entities/document/document.routes.ts new file mode 100644 index 0000000..b537e96 --- /dev/null +++ b/src/main/webapp/app/entities/document/document.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { DocumentComponent } from './list/document.component'; +import { DocumentDetailComponent } from './detail/document-detail.component'; +import { DocumentUpdateComponent } from './update/document-update.component'; +import DocumentResolve from './route/document-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; +import { DocumentService } from './service/document.service'; + +const documentRoute: Routes = [ + { + path: '', + component: DocumentComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: DocumentService, + resource: Resources.DOCUMENT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: DocumentDetailComponent, + resolve: { + document: DocumentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: DocumentService, + resource: Resources.DOCUMENT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: DocumentUpdateComponent, + resolve: { + document: DocumentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: DocumentService, + resource: Resources.DOCUMENT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: DocumentUpdateComponent, + resolve: { + document: DocumentResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: DocumentService, + resource: Resources.DOCUMENT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default documentRoute; diff --git a/src/main/webapp/app/entities/document/list/document.component.html b/src/main/webapp/app/entities/document/list/document.component.html new file mode 100644 index 0000000..ad7feeb --- /dev/null +++ b/src/main/webapp/app/entities/document/list/document.component.html @@ -0,0 +1,100 @@ +
+

+ Documents + +
+ + + +
+

+ + + + + + @if (documents?.length === 0) { +
+ Nenhum Documents encontrado +
+ } + + @if (documents && documents.length > 0) { +
+ + + + + + + + + + @for (document of documents; track trackId) { + + + + + + + } + +
+
+ Title + + +
+
+
+ Description + + +
+
{{ document.title }}{{ document.description }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/document/list/document.component.ts b/src/main/webapp/app/entities/document/list/document.component.ts new file mode 100644 index 0000000..7265668 --- /dev/null +++ b/src/main/webapp/app/entities/document/list/document.component.ts @@ -0,0 +1,136 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { DataUtils } from 'app/core/util/data-util.service'; + +import { IDocument, NewDocument } from '../document.model'; +import { EntityArrayResponseType, DocumentService } from '../service/document.service'; +import { DocumentDeleteDialogComponent } from '../delete/document-delete-dialog.component'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +@Component({ + standalone: true, + selector: 'jhi-document', + templateUrl: './document.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class DocumentComponent extends AbstractResilientEntityComponent implements OnInit { + subscription: Subscription | null = null; + documents?: IDocument[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected documentService = inject(DocumentService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected dataUtils = inject(DataUtils); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IDocument): number => this.documentService.getEntityIdentifier(item); + + constructor(service: DocumentService) { super(service); } + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.documents || this.documents.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + return this.dataUtils.openFile(base64String, contentType); + } + + delete(document: IDocument): void { + const modalRef = this.modalService.open(DocumentDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.document = document; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.documents = this.refineData(dataFromBody); + } + + protected refineData(data: IDocument[]): IDocument[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IDocument[] | null): IDocument[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.documentService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/document/route/document-routing-resolve.service.ts b/src/main/webapp/app/entities/document/route/document-routing-resolve.service.ts new file mode 100644 index 0000000..959ed48 --- /dev/null +++ b/src/main/webapp/app/entities/document/route/document-routing-resolve.service.ts @@ -0,0 +1,44 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { IDocument } from '../document.model'; +import { DocumentService } from '../service/document.service'; + +const documentResolve = (route: ActivatedRouteSnapshot): Observable => { + const router = inject(Router); + const translate = inject(TranslateService); + const documentService = inject(DocumentService); + const id = route.params['id']; + if (id) { + return inject(DocumentService) + .find(id) + .pipe( + mergeMap((documentResponse: HttpResponse) => { + if (documentResponse.body) { + const document = documentResponse.body; + + // Check if is /edit route, and the state of the Document + const path = route.routeConfig?.path; + if (path && path.endsWith('/edit') && !documentService.canUpdate(document)) { + router.navigate(['/document', id, 'view'], { + state: { message: translate.instant('resilientApp.document.home.editNotAllowedForState') } + }); + return EMPTY; + } else { + return of(document); + } + } else { + router.navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default documentResolve; diff --git a/src/main/webapp/app/entities/document/service/document.service.ts b/src/main/webapp/app/entities/document/service/document.service.ts new file mode 100644 index 0000000..bcbea8f --- /dev/null +++ b/src/main/webapp/app/entities/document/service/document.service.ts @@ -0,0 +1,20 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum } from 'app/resilient/resilient-base/resilient-base.service'; +import { IDocument, NewDocument } from '../document.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class DocumentService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/documents'); + override getResourceUrl(): string { return this.resourceUrl; } +} diff --git a/src/main/webapp/app/entities/document/update/document-update-form.service.ts b/src/main/webapp/app/entities/document/update/document-update-form.service.ts new file mode 100644 index 0000000..eec1b0b --- /dev/null +++ b/src/main/webapp/app/entities/document/update/document-update-form.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IDocument, NewDocument } from '../document.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IDocument for edit and NewDocumentFormGroupInput for create. + */ +type DocumentFormGroupInput = IDocument | PartialWithRequiredKeyOf; + +type DocumentFormDefaults = Pick; + +type DocumentFormGroupContent = { + id: FormControl; + title: FormControl; + description: FormControl; + dataFile: FormControl; + dataFileContentType: FormControl; +}; + +export type DocumentFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class DocumentFormService { + createDocumentFormGroup(document: DocumentFormGroupInput = { id: null }): DocumentFormGroup { + const documentRawValue = { + ...this.getFormDefaults(), + ...document, + }; + return new FormGroup({ + id: new FormControl( + { value: documentRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + title: new FormControl(documentRawValue.title, { + validators: [Validators.required], + }), + description: new FormControl(documentRawValue.description), + dataFile: new FormControl(documentRawValue.dataFile, { + validators: [Validators.required], + }), + dataFileContentType: new FormControl(documentRawValue.dataFileContentType), + }); + } + + getDocument(form: DocumentFormGroup): IDocument | NewDocument { + return form.getRawValue() as IDocument | NewDocument; + } + + resetForm(form: DocumentFormGroup, document: DocumentFormGroupInput): void { + const documentRawValue = { + ...this.getFormDefaults(), + ...document + }; + form.reset( + { + ...documentRawValue, + id: { value: documentRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): DocumentFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/document/update/document-update.component.html b/src/main/webapp/app/entities/document/update/document-update.component.html new file mode 100644 index 0000000..5a14a0a --- /dev/null +++ b/src/main/webapp/app/entities/document/update/document-update.component.html @@ -0,0 +1,114 @@ +
+
+
+

+ Criar ou editar Document +

+ +
+ + +
+ + + @if (editForm.get('title')!.invalid && (editForm.get('title')!.dirty || editForm.get('title')!.touched)) { +
+ @if (editForm.get('title')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + +
+
+ +
+ @if (editForm.get('dataFile')!.value) { +
+ Abrir
+ {{ editForm.get('dataFileContentType')!.value }}, {{ byteSize(editForm.get('dataFile')!.value!) }} + +
+ } + +
+ + + @if (editForm.get('dataFile')!.invalid && (editForm.get('dataFile')!.dirty || editForm.get('dataFile')!.touched)) { +
+ @if (editForm.get('dataFile')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/document/update/document-update.component.ts b/src/main/webapp/app/entities/document/update/document-update.component.ts new file mode 100644 index 0000000..bd7ab81 --- /dev/null +++ b/src/main/webapp/app/entities/document/update/document-update.component.ts @@ -0,0 +1,102 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, first, Subscription } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { AlertError } from 'app/shared/alert/alert-error.model'; +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; +import { DataUtils, FileLoadError } from 'app/core/util/data-util.service'; + +import { DocumentService } from '../service/document.service'; +import { IDocument } from '../document.model'; +import { DocumentFormService, DocumentFormGroup } from './document-update-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-document', + templateUrl: './document-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class DocumentUpdateComponent implements OnInit { + isSaving = false; + isLoading = false; + document: IDocument | null = null; + + protected dataUtils = inject(DataUtils); + protected eventManager = inject(EventManager); + protected documentService = inject(DocumentService); + protected documentFormService = inject(DocumentFormService); + protected activatedRoute = inject(ActivatedRoute); + protected router = inject(Router); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: DocumentFormGroup = this.documentFormService.createDocumentFormGroup(); + + ngOnInit(): void { + this.isLoading=true; + + this.activatedRoute.data.subscribe(({ document }) => { + this.document = document; + if (document) { + this.updateForm(document); + } + }); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + setFileData(event: Event, field: string, isImage: boolean): void { + this.dataUtils.loadFileToForm(event, this.editForm, field, isImage).subscribe({ + error: (err: FileLoadError) => + this.eventManager.broadcast(new EventWithContent('resilientApp.error', { ...err, key: 'error.file.' + err.key })), + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const document = this.documentFormService.getDocument(this.editForm); + if (document.id !== null) { + this.subscribeToSaveResponse(this.documentService.update(document)); + } else { + this.subscribeToSaveResponse(this.documentService.create(document)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(document: IDocument): void { + this.document = document; + this.documentFormService.resetForm(this.editForm, document); + } +} diff --git a/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.html b/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.html new file mode 100644 index 0000000..9cd7c49 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (emissionFactor) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.spec.ts new file mode 100644 index 0000000..70254e8 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { EmissionFactorService } from '../service/emission-factor.service'; + +import { EmissionFactorDeleteDialogComponent } from './emission-factor-delete-dialog.component'; + +describe('EmissionFactor Management Delete Component', () => { + let comp: EmissionFactorDeleteDialogComponent; + let fixture: ComponentFixture; + let service: EmissionFactorService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, EmissionFactorDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(EmissionFactorDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(EmissionFactorDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(EmissionFactorService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.ts b/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.ts new file mode 100644 index 0000000..b0d5555 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/delete/emission-factor-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IEmissionFactor } from '../emission-factor.model'; +import { EmissionFactorService } from '../service/emission-factor.service'; + +@Component({ + standalone: true, + templateUrl: './emission-factor-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class EmissionFactorDeleteDialogComponent { + emissionFactor?: IEmissionFactor; + + protected emissionFactorService = inject(EmissionFactorService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.emissionFactorService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.html b/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.html new file mode 100644 index 0000000..9c62d4a --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.html @@ -0,0 +1,106 @@ +
+
+ @if (emissionFactor()) { +
+

Emission Factor

+ +
+ + + + + +
+
+ Year +
+
+ {{ emissionFactor()!.year }} +
+
Variable Scope
+
+ @if (emissionFactor()!.variableScope) { + + } +
+
Variable Category
+
+ @if (emissionFactor()!.variableCategory) { + + } +
+
+ Code +
+
+ {{ emissionFactor()!.code }} +
+
+ Name +
+
+ {{ emissionFactor()!.name }} +
+
+ Unit +
+
+ {{ emissionFactor()!.unit }} +
+
+ Value +
+
+ {{ emissionFactor()!.value }} +
+
+ Source +
+
+ {{ emissionFactor()!.source }} +
+
+ Comments +
+
+ {{ emissionFactor()!.comments }} +
+
+ + + + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.spec.ts b/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.spec.ts new file mode 100644 index 0000000..e1c92b3 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { EmissionFactorDetailComponent } from './emission-factor-detail.component'; + +describe('EmissionFactor Management Detail Component', () => { + let comp: EmissionFactorDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmissionFactorDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: EmissionFactorDetailComponent, + resolve: { emissionFactor: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(EmissionFactorDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EmissionFactorDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load emissionFactor on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', EmissionFactorDetailComponent); + + // THEN + expect(instance.emissionFactor()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.ts b/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.ts new file mode 100644 index 0000000..bfcc0e3 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/detail/emission-factor-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IEmissionFactor } from '../emission-factor.model'; + +@Component({ + standalone: true, + selector: 'jhi-emission-factor-detail', + templateUrl: './emission-factor-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class EmissionFactorDetailComponent { + emissionFactor = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/emission-factor/emission-factor.model.ts b/src/main/webapp/app/entities/emission-factor/emission-factor.model.ts new file mode 100644 index 0000000..3765974 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/emission-factor.model.ts @@ -0,0 +1,31 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; + +export interface IEmissionFactor extends IResilientEntity { + year?: number | null; + code?: string | null; + name?: string | null; + unit?: string | null; + value?: number | null; + source?: string | null; + comments?: string | null; + variableScope?: Pick | null; + variableCategory?: Pick | null; +} + +export type NewEmissionFactor = Omit & { id: null }; + +export type SearchEmissionFactor = Omit< + IEmissionFactor, + // Remove unwanted properties for search + 'id' | 'variableScope' | 'variableCategory' > + & + // Add specific properties for search + { + id?: number | null; + emissionFactorNameOrCode?: string | null; + variableScopeId?: number | null; + variableCategoryId?: number | null; + } diff --git a/src/main/webapp/app/entities/emission-factor/emission-factor.routes.ts b/src/main/webapp/app/entities/emission-factor/emission-factor.routes.ts new file mode 100644 index 0000000..1a6152c --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/emission-factor.routes.ts @@ -0,0 +1,94 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { EmissionFactorComponent } from './list/emission-factor.component'; +import { EmissionFactorPublicComponent } from './list/public/emission-factor-public.component'; +import { EmissionFactorDetailComponent } from './detail/emission-factor-detail.component'; +import { EmissionFactorUpdateComponent } from './update/emission-factor-update.component'; +import EmissionFactorResolve from './route/emission-factor-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; +import { EmissionFactorService } from './service/emission-factor.service'; + +const emissionFactorRoute: Routes = [ + { + path: '', + component: EmissionFactorComponent, + data: { + defaultSort: 'id,' + ASC, + }, + canActivate: [UserRouteAccessService], + }, + + { + path: 'list', + component: EmissionFactorPublicComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: EmissionFactorService, + resource: Resources.EMISSION_FACTOR, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR', 'ROLE_USER'], + } + }, + { + path: ':id/view', + component: EmissionFactorDetailComponent, + resolve: { + emissionFactor: EmissionFactorResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: EmissionFactorService, + resource: Resources.EMISSION_FACTOR, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: EmissionFactorUpdateComponent, + resolve: { + emissionFactor: EmissionFactorResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: EmissionFactorService, + resource: Resources.EMISSION_FACTOR, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: EmissionFactorUpdateComponent, + resolve: { + emissionFactor: EmissionFactorResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: EmissionFactorService, + resource: Resources.EMISSION_FACTOR, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/clone', + component: EmissionFactorUpdateComponent, + resolve: { + emissionFactor: EmissionFactorResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: EmissionFactorService, + resource: Resources.EMISSION_FACTOR, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default emissionFactorRoute; diff --git a/src/main/webapp/app/entities/emission-factor/emission-factor.test-samples.ts b/src/main/webapp/app/entities/emission-factor/emission-factor.test-samples.ts new file mode 100644 index 0000000..e69b688 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/emission-factor.test-samples.ts @@ -0,0 +1,41 @@ +import { IEmissionFactor, NewEmissionFactor } from './emission-factor.model'; + +export const sampleWithRequiredData: IEmissionFactor = { + id: 31193, + code: '9963', + name: 'loftily administration barring', + unit: 'aha boo times', + value: 15279.81, +}; + +export const sampleWithPartialData: IEmissionFactor = { + id: 8501, + code: '4559', + name: 'strange roller', + unit: 'closely cork for', + value: 29094.42, + source: 'surprisingly anthologize', +}; + +export const sampleWithFullData: IEmissionFactor = { + id: 8523, + code: '23746', + name: 'webbed', + unit: 'pro', + value: 12837.82, + source: 'glistening', + comments: 'garage', +}; + +export const sampleWithNewData: NewEmissionFactor = { + code: '1', + name: 'babyish', + unit: 'crocodile toward where', + value: 13931.59, + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/emission-factor/list/emission-factor-search-form.service.spec.ts b/src/main/webapp/app/entities/emission-factor/list/emission-factor-search-form.service.spec.ts new file mode 100644 index 0000000..8cafbc6 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/emission-factor-search-form.service.spec.ts @@ -0,0 +1,104 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../emission-factor.test-samples'; + +import { EmissionFactorSearchFormService } from './emission-factor-search-form.service'; + +describe('EmissionFactor Search Form Service', () => { + let service: EmissionFactorSearchFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EmissionFactorSearchFormService); + }); + + describe('Service methods', () => { + describe('createEmissionFactorsearchFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createEmissionFactorSearchFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + emissionFactorNameOrCode: expect.any(Object), + year: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + unit: expect.any(Object), + source: expect.any(Object), + comments: expect.any(Object), + version: expect.any(Object), + variableScopeId: expect.any(Object), + variableCategoryId: expect.any(Object), + }), + ); + }); + + it('passing SearchEmissionFactor should create a new form with FormGroup', () => { + const formGroup = service.createEmissionFactorSearchFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + emissionFactorNameOrCode: expect.any(Object), + year: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + unit: expect.any(Object), + source: expect.any(Object), + comments: expect.any(Object), + version: expect.any(Object), + variableScopeId: expect.any(Object), + variableCategoryId: expect.any(Object), + }), + ); + }); + }); + + describe('getEmissionFactor', () => { + it('should return SearchEmissionFactor for default EmissionFactor initial value', () => { + const formGroup = service.createEmissionFactorSearchFormGroup(sampleWithNewData); + + const emissionFactor = service.getEmissionFactor(formGroup) as any; + + expect(emissionFactor).toMatchObject(sampleWithNewData); + }); + + it('should return SearchEmissionFactor for empty EmissionFactor initial value', () => { + const formGroup = service.createEmissionFactorSearchFormGroup(); + + const emissionFactor = service.getEmissionFactor(formGroup) as any; + + expect(emissionFactor).toMatchObject({}); + }); + + it('should return SearchEmissionFactor', () => { + const formGroup = service.createEmissionFactorSearchFormGroup(sampleWithRequiredData); + + const emissionFactor = service.getEmissionFactor(formGroup) as any; + + expect(emissionFactor).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IEmissionFactor should not enable id FormControl', () => { + const formGroup = service.createEmissionFactorSearchFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing SearchEmissionFactor should disable id FormControl', () => { + const formGroup = service.createEmissionFactorSearchFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/list/emission-factor-search-form.service.ts b/src/main/webapp/app/entities/emission-factor/list/emission-factor-search-form.service.ts new file mode 100644 index 0000000..2a88434 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/emission-factor-search-form.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { SearchEmissionFactor } from '../emission-factor.model'; +import { SearchFormService } from 'app/entities/shared/shared-search-form.service'; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts SearchUnit, for search. in the list + */ +type EmissionFactorSearchFormGroupInput = SearchEmissionFactor; + +type EmissionFactorSearchFormDefaults = SearchEmissionFactor; + +type EmissionFactorSearchFormGroupContent = { + id: FormControl; + year: FormControl; + emissionFactorNameOrCode: FormControl; + code: FormControl; + name: FormControl; + unit: FormControl; + source: FormControl; + comments: FormControl; + version: FormControl; + variableScopeId: FormControl; + variableCategoryId: FormControl; +}; + +export type EmissionFactorSearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class EmissionFactorSearchFormService extends SearchFormService { + createEmissionFactorSearchFormGroup(emissionFactor: EmissionFactorSearchFormGroupInput = { id: null }): EmissionFactorSearchFormGroup { + const emissionFactorRawValue = { + ...this.getSearchFormDefaults(), + ...emissionFactor, + }; + return new FormGroup({ + id: new FormControl(emissionFactorRawValue.id), + year: new FormControl(emissionFactorRawValue.year, { + validators: [Validators.required, Validators.pattern('^[0-9]*$')], + }), + emissionFactorNameOrCode: new FormControl(emissionFactorRawValue.emissionFactorNameOrCode), + code: new FormControl(emissionFactorRawValue.code), + name: new FormControl(emissionFactorRawValue.name), + unit: new FormControl(emissionFactorRawValue.unit), + source: new FormControl(emissionFactorRawValue.source), + comments: new FormControl(emissionFactorRawValue.comments), + version: new FormControl(emissionFactorRawValue.version), + variableScopeId: new FormControl(emissionFactorRawValue.variableScopeId, { + validators: [Validators.required], + }), + variableCategoryId: new FormControl(emissionFactorRawValue.variableCategoryId), + }); + } + + getEmissionFactor(form: EmissionFactorSearchFormGroup): SearchEmissionFactor { + return { + ...form.getRawValue(), + year: form.getRawValue().year ? Number(form.getRawValue().year) : null, + }; + //return form.getRawValue() as SearchEmissionFactor; + } + + resetForm(form: EmissionFactorSearchFormGroup, emissionFactor: EmissionFactorSearchFormGroupInput): void { + const emissionFactorRawValue = { ...this.getSearchFormDefaults(), ...emissionFactor }; + form.reset( + { + ...emissionFactorRawValue, + id: { value: emissionFactorRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): EmissionFactorSearchFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.html b/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.html new file mode 100644 index 0000000..9f5d06e --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.html @@ -0,0 +1,211 @@ +
+

+ Emission Factors + +
+ + + +
+

+ + + + + + @if (emissionFactors && emissionFactors.length >= 0) { +
+ {{ ' ' // Wrap the all table in a form. This is the only way reactiveforms will work }} +
+ + + + {{ ' ' // Header row }} + + + + + + + + + + + + + {{ ' ' // Search row }} + + + + + + + + + + + + + + @for (emissionFactor of emissionFactors; track trackId) { + + + + + + + + + + + + + } + +
+
+ Year + + +
+
+
+ Variable Scope + +
+
+
+ Variable Category + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Unit + + +
+
+
+ Value + + +
+
+ + + + + + + + + + + + + +
{{ emissionFactor.year }} + @if (emissionFactor.variableScope) { + + } + + @if (emissionFactor.variableCategory) { + + } + {{ emissionFactor.code }}{{ emissionFactor.name }}{{ emissionFactor.unit }}{{ emissionFactor.value }} +
+ + + Visualizar + + + + + Editar + + + +
+
+ +
+
+ } + + @if (emissionFactors?.length === 0) { +
+ Nenhum Emission Factors encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.spec.ts b/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.spec.ts new file mode 100644 index 0000000..da4c2db --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../emission-factor.test-samples'; +import { EmissionFactorService } from '../service/emission-factor.service'; + +import { EmissionFactorComponent } from './emission-factor.component'; +import SpyInstance = jest.SpyInstance; + +describe('EmissionFactor Management Component', () => { + let comp: EmissionFactorComponent; + let fixture: ComponentFixture; + let service: EmissionFactorService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, EmissionFactorComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(EmissionFactorComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(EmissionFactorComponent); + comp = fixture.componentInstance; + service = TestBed.inject(EmissionFactorService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.emissionFactors?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to emissionFactorService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getEmissionFactorIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getEmissionFactorIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.ts b/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.ts new file mode 100644 index 0000000..f373cc0 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/emission-factor.component.ts @@ -0,0 +1,180 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, map } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; + + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IEmissionFactor } from '../emission-factor.model'; +import { EntityArrayResponseType, EmissionFactorService } from '../service/emission-factor.service'; +import { EmissionFactorDeleteDialogComponent } from '../delete/emission-factor-delete-dialog.component'; + +import { EmissionFactorSearchFormService, EmissionFactorSearchFormGroup } from './emission-factor-search-form.service'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; + + +@Component({ + standalone: true, + selector: 'jhi-emission-factor', + templateUrl: './emission-factor.component.html', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class EmissionFactorComponent implements OnInit { + subscription: Subscription | null = null; + emissionFactors?: IEmissionFactor[]; + isLoading = false; + + sortState = sortStateSignal({}); + + variableScopesSharedCollection: IVariableScope[] = []; + variableCategoriesSharedCollection: IVariableCategory[] = []; + + public router = inject(Router); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + protected emissionFactorService = inject(EmissionFactorService); + protected emissionFactorSearchFormService = inject(EmissionFactorSearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + searchForm: EmissionFactorSearchFormGroup = this.emissionFactorSearchFormService.createEmissionFactorSearchFormGroup(); + trackId = (_index: number, item: IEmissionFactor): number => this.emissionFactorService.getEntityIdentifier(item); + + compareVariableScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + compareVariableCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.emissionFactors || this.emissionFactors.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + + // Listen to changes to the scope filter property + this.searchForm.get('variableScopeId')?.valueChanges.subscribe((scopeId) => { + this.loadCategoriesOfSelectedScope(scopeId); + this.searchForm.get('variableCategoryId')?.setValue(null); + }); + + this.loadRelationshipsOptions(); + } + + delete(emissionFactor: IEmissionFactor): void { + const modalRef = this.modalService.open(EmissionFactorDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.emissionFactor = emissionFactor; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + onSearch(): void { + this.load(); + } + + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.emissionFactors = this.refineData(dataFromBody); + } + + protected refineData(data: IEmissionFactor[]): IEmissionFactor[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IEmissionFactor[] | null): IEmissionFactor[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + + const searchModel = this.emissionFactorSearchFormService.getEmissionFactor(this.searchForm); + this.emissionFactorSearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + + return this.emissionFactorService.queryByCriteria(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } + + protected loadCategoriesOfSelectedScope(scopeId: number|null|undefined): void { + if (scopeId) { + this.variableCategoryService + .queryByScopeForFactor(scopeId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((cats: IVariableCategory[]) => (this.variableCategoriesSharedCollection = cats)); + } else { + this.variableCategoriesSharedCollection = []; + } + } + + protected loadRelationshipsOptions(): void { + this.variableScopeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((variableScopes: IVariableScope[]) => (this.variableScopesSharedCollection = variableScopes)); + } +} diff --git a/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.css b/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.css new file mode 100644 index 0000000..730667d --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.css @@ -0,0 +1,44 @@ +.search-form { + display: grid; + grid-template-columns: repeat(4, 1fr); /* 4 equal columns */ + gap: 15px; + align-items: end; +} + +.form-group { + display: flex; + flex-direction: column; +} + +label { + font-weight: bold; + margin-bottom: 5px; +} + +select, button { + padding: 8px; + font-size: 16px; + /* width: 100%; */ /* Makes sure they fill the column */ +} + +button { + cursor: pointer; + background-color: #007BFF; + color: white; + border: none; + padding: 10px; + font-weight: bold; +} + +button:hover { + background-color: #0056b3; +} + +.btn-refresh { + min-width: 200px; + margin: 0px; +} + +#searchForm .form-group { + width: 100%; +} diff --git a/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.html b/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.html new file mode 100644 index 0000000..f7a3fc4 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.html @@ -0,0 +1,157 @@ +
+

+ Emission Factors + +
+ +
+

+ + + + + + {{ ' ' // Search form }} +
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+
+ + @if (emissionFactors) { +
+ + + + + + + + + + + + @for (emissionFactor of emissionFactors; track trackId) { + + + + + + + } + +
+
+ Category +
+
+
+ Name +
+
+
+ Valor +    + +
+
+
+ Unidade +
+
+ @if (emissionFactor.variableCategory) { + {{ emissionFactor.variableCategory.name }} + } + + @if (emissionFactor.name) { + {{ emissionFactor.name }} + } + + @if (emissionFactor.value) { + {{ emissionFactor.value }} + } + + @if (emissionFactor.unit) { + {{ emissionFactor.unit }} + } +
+
+ } + + @if (emissionFactors?.length === 0) { +
+ Nenhum Fator de Emissão encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.ts b/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.ts new file mode 100644 index 0000000..e734f64 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/list/public/emission-factor-public.component.ts @@ -0,0 +1,180 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, map, of } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IEmissionFactor } from '../../emission-factor.model'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { EntityArrayResponseType, EmissionFactorService } from '../../service/emission-factor.service'; +import { EmissionFactorSearchFormService, EmissionFactorSearchFormGroup } from '../emission-factor-search-form.service'; +import { FormatHelper } from 'app/shared/utils/format-helper'; +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; + +@Component({ + standalone: true, + selector: 'jhi-emission-factor-public', + templateUrl: './emission-factor-public.component.html', + styleUrl: './emission-factor-public.component.css', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + MatProgressSpinnerModule + ], +}) +export class EmissionFactorPublicComponent implements OnInit { + subscriptionWorkOrganization!: Subscription; // Store the subscription + subscription: Subscription | null = null; + emissionFactors?: IEmissionFactor[]; + isLoading = false; + isExporting: boolean = false; + + variableScopesSharedCollection: IVariableScope[] = []; + categoriesSharedCollection: IVariableCategory[] = []; + periodYears: number[] = []; + + public router = inject(Router); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + protected periodService = inject(PeriodService); + protected emissionFactorService = inject(EmissionFactorService); + protected emissionFactorSearchFormService = inject(EmissionFactorSearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + searchForm: EmissionFactorSearchFormGroup = this.emissionFactorSearchFormService.createEmissionFactorSearchFormGroup(); + trackId = (_index: number, item: IEmissionFactor): number => this.emissionFactorService.getEntityIdentifier(item); + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => + this.periodService.compareEntity(o1, o2); + + compareCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + compareVariableScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + ngOnInit(): void { + // Listen to changes to the scope filter property + this.searchForm.get('variableScopeId')?.valueChanges.subscribe((scopeId) => { + this.loadCategoriesOfSelectedScope(scopeId); + this.searchForm.get('variableCategoryId')?.setValue(null); + }); + + this.loadRelationshipsOptions(); + } + + onSearch(): void { + this.load(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + + this.emissionFactors = dataFromBody; + } + + protected fillComponentAttributesFromResponseBody(data: IEmissionFactor[] | null): IEmissionFactor[] { + return data ?? []; + } + + protected queryBackend(): Observable { + const searchModel = this.emissionFactorSearchFormService.getEmissionFactor(this.searchForm); + + // Without YEAR, can't search + if (!searchModel.year) return of(new HttpResponse({ body: [] })); + + let searchScopeId = "ALL"; + if (searchModel.variableScopeId) searchScopeId = searchModel.variableScopeId.toString(); + + let searchCategoryId = "ALL"; + if (searchModel.variableCategoryId) searchCategoryId = searchModel.variableCategoryId.toString(); + + return this.emissionFactorService.queryBySearch(searchModel.year, searchScopeId, searchCategoryId).pipe(tap(() => (this.isLoading = false))); + } + + protected loadCategoriesOfSelectedScope(scopeId: number|null|undefined): void { + if (scopeId) { + this.variableCategoryService + .queryByScopeForFactor(scopeId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((cats: IVariableCategory[]) => (this.categoriesSharedCollection = cats)); + } else { + this.categoriesSharedCollection = []; + } + } + + protected loadRelationshipsOptions(): void { + this.variableScopeService + .queryForFactor() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((scopes: IVariableScope[]) => (this.variableScopesSharedCollection = scopes)); + + this.periodService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((periods: IPeriod[]) => { + this.periodYears = Array.from( + new Set( + periods + .map(period => period.endDate?.year()) // extract year + .filter((year): year is number => year !== undefined && year !== null) // remove undefined/null + ) + ).sort((a, b) => b - a); // optional: sort years descending + + // Set first Year as default. It's mandatory to select a Year + this.searchForm.get('year')?.setValue(this.periodYears[0]); + }); + } + + formatValue(value: number): string { + return FormatHelper.formatDecimalValue(value); + } + + export(): void { + this.isExporting = true; + const queryObject: any = { + eagerload: true, + }; + + const searchModel = this.emissionFactorSearchFormService.getEmissionFactor(this.searchForm); + if (searchModel) { + // Apply conditions + this.emissionFactorSearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + } + + this.emissionFactorService.export(queryObject).subscribe((blob: Blob) => { + const a = document.createElement('a'); + const objectUrl = URL.createObjectURL(blob); + a.href = objectUrl; + a.download = 'report.xlsx'; + a.click(); + URL.revokeObjectURL(objectUrl); + + this.isExporting = false; + }); + } +} diff --git a/src/main/webapp/app/entities/emission-factor/route/emission-factor-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/emission-factor/route/emission-factor-routing-resolve.service.spec.ts new file mode 100644 index 0000000..be448c4 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/route/emission-factor-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IEmissionFactor } from '../emission-factor.model'; +import { EmissionFactorService } from '../service/emission-factor.service'; + +import emissionFactorResolve from './emission-factor-routing-resolve.service'; + +describe('EmissionFactor routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: EmissionFactorService; + let resultEmissionFactor: IEmissionFactor | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(EmissionFactorService); + resultEmissionFactor = undefined; + }); + + describe('resolve', () => { + it('should return IEmissionFactor returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + emissionFactorResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultEmissionFactor = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultEmissionFactor).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + emissionFactorResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultEmissionFactor = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultEmissionFactor).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + emissionFactorResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultEmissionFactor = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultEmissionFactor).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/route/emission-factor-routing-resolve.service.ts b/src/main/webapp/app/entities/emission-factor/route/emission-factor-routing-resolve.service.ts new file mode 100644 index 0000000..956bc10 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/route/emission-factor-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IEmissionFactor } from '../emission-factor.model'; +import { EmissionFactorService } from '../service/emission-factor.service'; + +const emissionFactorResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(EmissionFactorService) + .find(id) + .pipe( + mergeMap((emissionFactor: HttpResponse) => { + if (emissionFactor.body) { + return of(emissionFactor.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default emissionFactorResolve; diff --git a/src/main/webapp/app/entities/emission-factor/service/emission-factor.service.spec.ts b/src/main/webapp/app/entities/emission-factor/service/emission-factor.service.spec.ts new file mode 100644 index 0000000..6da0a74 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/service/emission-factor.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IEmissionFactor } from '../emission-factor.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../emission-factor.test-samples'; + +import { EmissionFactorService } from './emission-factor.service'; + +const requireRestSample: IEmissionFactor = { + ...sampleWithRequiredData, +}; + +describe('EmissionFactor Service', () => { + let service: EmissionFactorService; + let httpMock: HttpTestingController; + let expectedResult: IEmissionFactor | IEmissionFactor[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(EmissionFactorService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a EmissionFactor', () => { + const emissionFactor = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(emissionFactor).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a EmissionFactor', () => { + const emissionFactor = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(emissionFactor).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a EmissionFactor', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of EmissionFactor', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a EmissionFactor', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addEmissionFactorToCollectionIfMissing', () => { + it('should add a EmissionFactor to an empty array', () => { + const emissionFactor: IEmissionFactor = sampleWithRequiredData; + expectedResult = service.addEmissionFactorToCollectionIfMissing([], emissionFactor); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(emissionFactor); + }); + + it('should not add a EmissionFactor to an array that contains it', () => { + const emissionFactor: IEmissionFactor = sampleWithRequiredData; + const emissionFactorCollection: IEmissionFactor[] = [ + { + ...emissionFactor, + }, + sampleWithPartialData, + ]; + expectedResult = service.addEmissionFactorToCollectionIfMissing(emissionFactorCollection, emissionFactor); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a EmissionFactor to an array that doesn't contain it", () => { + const emissionFactor: IEmissionFactor = sampleWithRequiredData; + const emissionFactorCollection: IEmissionFactor[] = [sampleWithPartialData]; + expectedResult = service.addEmissionFactorToCollectionIfMissing(emissionFactorCollection, emissionFactor); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(emissionFactor); + }); + + it('should add only unique EmissionFactor to an array', () => { + const emissionFactorArray: IEmissionFactor[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const emissionFactorCollection: IEmissionFactor[] = [sampleWithRequiredData]; + expectedResult = service.addEmissionFactorToCollectionIfMissing(emissionFactorCollection, ...emissionFactorArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const emissionFactor: IEmissionFactor = sampleWithRequiredData; + const emissionFactor2: IEmissionFactor = sampleWithPartialData; + expectedResult = service.addEmissionFactorToCollectionIfMissing([], emissionFactor, emissionFactor2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(emissionFactor); + expect(expectedResult).toContain(emissionFactor2); + }); + + it('should accept null and undefined values', () => { + const emissionFactor: IEmissionFactor = sampleWithRequiredData; + expectedResult = service.addEmissionFactorToCollectionIfMissing([], null, emissionFactor, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(emissionFactor); + }); + + it('should return initial array if no EmissionFactor is added', () => { + const emissionFactorCollection: IEmissionFactor[] = [sampleWithRequiredData]; + expectedResult = service.addEmissionFactorToCollectionIfMissing(emissionFactorCollection, undefined, null); + expect(expectedResult).toEqual(emissionFactorCollection); + }); + }); + + describe('compareEmissionFactor', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareEmissionFactor(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareEmissionFactor(entity1, entity2); + const compareResult2 = service.compareEmissionFactor(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareEmissionFactor(entity1, entity2); + const compareResult2 = service.compareEmissionFactor(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareEmissionFactor(entity1, entity2); + const compareResult2 = service.compareEmissionFactor(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/service/emission-factor.service.ts b/src/main/webapp/app/entities/emission-factor/service/emission-factor.service.ts new file mode 100644 index 0000000..3d7e64f --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/service/emission-factor.service.ts @@ -0,0 +1,31 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IEmissionFactor, NewEmissionFactor } from '../emission-factor.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class EmissionFactorService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/emission-factors'); + override getResourceUrl(): string { return this.resourceUrl; } + + export(req?: any): Observable { + const options = createRequestOption(req); + + return this.http.get(`${this.resourceUrl}/export`, { params: options, responseType: 'blob' }); + } + + queryBySearch(year: number, scopeId: string, categoryId: string): Observable> { + return this.http.get(`${this.resourceUrl}/search/${year}/${scopeId}/${categoryId}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/emission-factor/update/emission-factor-form.service.spec.ts b/src/main/webapp/app/entities/emission-factor/update/emission-factor-form.service.spec.ts new file mode 100644 index 0000000..e5e896c --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/update/emission-factor-form.service.spec.ts @@ -0,0 +1,100 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../emission-factor.test-samples'; + +import { EmissionFactorFormService } from './emission-factor-form.service'; + +describe('EmissionFactor Form Service', () => { + let service: EmissionFactorFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EmissionFactorFormService); + }); + + describe('Service methods', () => { + describe('createEmissionFactorFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createEmissionFactorFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + unit: expect.any(Object), + value: expect.any(Object), + source: expect.any(Object), + comments: expect.any(Object), + variableScope: expect.any(Object), + variableCategory: expect.any(Object), + }), + ); + }); + + it('passing IEmissionFactor should create a new form with FormGroup', () => { + const formGroup = service.createEmissionFactorFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + unit: expect.any(Object), + value: expect.any(Object), + source: expect.any(Object), + comments: expect.any(Object), + variableScope: expect.any(Object), + variableCategory: expect.any(Object), + }), + ); + }); + }); + + describe('getEmissionFactor', () => { + it('should return NewEmissionFactor for default EmissionFactor initial value', () => { + const formGroup = service.createEmissionFactorFormGroup(sampleWithNewData); + + const emissionFactor = service.getEmissionFactor(formGroup) as any; + + expect(emissionFactor).toMatchObject(sampleWithNewData); + }); + + it('should return NewEmissionFactor for empty EmissionFactor initial value', () => { + const formGroup = service.createEmissionFactorFormGroup(); + + const emissionFactor = service.getEmissionFactor(formGroup) as any; + + expect(emissionFactor).toMatchObject({}); + }); + + it('should return IEmissionFactor', () => { + const formGroup = service.createEmissionFactorFormGroup(sampleWithRequiredData); + + const emissionFactor = service.getEmissionFactor(formGroup) as any; + + expect(emissionFactor).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IEmissionFactor should not enable id FormControl', () => { + const formGroup = service.createEmissionFactorFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewEmissionFactor should disable id FormControl', () => { + const formGroup = service.createEmissionFactorFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/update/emission-factor-form.service.ts b/src/main/webapp/app/entities/emission-factor/update/emission-factor-form.service.ts new file mode 100644 index 0000000..a7777e9 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/update/emission-factor-form.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IEmissionFactor, NewEmissionFactor } from '../emission-factor.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IEmissionFactor for edit and NewEmissionFactorFormGroupInput for create. + */ +type EmissionFactorFormGroupInput = IEmissionFactor | PartialWithRequiredKeyOf; + +type EmissionFactorFormDefaults = Pick; + +type EmissionFactorFormGroupContent = { + id: FormControl; + year: FormControl; + code: FormControl; + name: FormControl; + unit: FormControl; + value: FormControl; + source: FormControl; + comments: FormControl; + variableScope: FormControl; + variableCategory: FormControl; + version: FormControl; +}; + +export type EmissionFactorFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class EmissionFactorFormService { + createEmissionFactorFormGroup(emissionFactor: EmissionFactorFormGroupInput = { id: null }): EmissionFactorFormGroup { + const emissionFactorRawValue = { + ...this.getFormDefaults(), + ...emissionFactor, + }; + return new FormGroup({ + id: new FormControl( + { value: emissionFactorRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + year: new FormControl(emissionFactorRawValue.year, { + validators: [Validators.required, Validators.pattern('^[0-9]*$')], + }), + code: new FormControl(emissionFactorRawValue.code, { + validators: [Validators.required, Validators.pattern('^[0-9]*$')], + }), + name: new FormControl(emissionFactorRawValue.name, { + validators: [Validators.required], + }), + unit: new FormControl(emissionFactorRawValue.unit, { + validators: [Validators.required], + }), + value: new FormControl(emissionFactorRawValue.value, { + validators: [Validators.required], + }), + source: new FormControl(emissionFactorRawValue.source), + comments: new FormControl(emissionFactorRawValue.comments), + variableScope: new FormControl(emissionFactorRawValue.variableScope), + variableCategory: new FormControl(emissionFactorRawValue.variableCategory), + version: new FormControl(emissionFactorRawValue.version), + }); + } + + getEmissionFactor(form: EmissionFactorFormGroup): IEmissionFactor | NewEmissionFactor { + return form.getRawValue() as IEmissionFactor | NewEmissionFactor; + } + + resetForm(form: EmissionFactorFormGroup, emissionFactor: EmissionFactorFormGroupInput): void { + const emissionFactorRawValue = { ...this.getFormDefaults(), ...emissionFactor }; + form.reset( + { + ...emissionFactorRawValue, + id: { value: emissionFactorRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): EmissionFactorFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.html b/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.html new file mode 100644 index 0000000..d901910 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.html @@ -0,0 +1,207 @@ +
+
+
+

+ Criar ou editar Emission Factor +

+ +
+ + + @if (editForm.controls.id.value !== null && false) { +
+ + +
+ } + +
+ + @if (editForm.controls.id.value !== null) { + + } @else { + + @if (editForm.get('year')!.invalid && (editForm.get('year')!.dirty || editForm.get('year')!.touched)) { +
+ @if (editForm.get('year')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('year')?.errors?.pattern) { + Este campo deve estar dentro do seguinte padrão Ano. + } +
+ } + } +
+ +
+ + +
+ +
+ + +
+ +
+ + @if (editForm.controls.id.value !== null) { + + } @else { + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.pattern) { + Este campo deve estar dentro do seguinte padrão Code. + } +
+ } + } +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('unit')!.invalid && (editForm.get('unit')!.dirty || editForm.get('unit')!.touched)) { +
+ @if (editForm.get('unit')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('value')!.invalid && (editForm.get('value')!.dirty || editForm.get('value')!.touched)) { +
+ @if (editForm.get('value')?.errors?.required) { + O campo é obrigatório. + } + Este campo é do tipo número. +
+ } +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.spec.ts b/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.spec.ts new file mode 100644 index 0000000..2c2b779 --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.spec.ts @@ -0,0 +1,203 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { IEmissionFactor } from '../emission-factor.model'; +import { EmissionFactorService } from '../service/emission-factor.service'; +import { EmissionFactorFormService } from './emission-factor-form.service'; + +import { EmissionFactorUpdateComponent } from './emission-factor-update.component'; + +describe('EmissionFactor Management Update Component', () => { + let comp: EmissionFactorUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let emissionFactorFormService: EmissionFactorFormService; + let emissionFactorService: EmissionFactorService; + let variableScopeService: VariableScopeService; + let variableCategoryService: VariableCategoryService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, EmissionFactorUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(EmissionFactorUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(EmissionFactorUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + emissionFactorFormService = TestBed.inject(EmissionFactorFormService); + emissionFactorService = TestBed.inject(EmissionFactorService); + variableScopeService = TestBed.inject(VariableScopeService); + variableCategoryService = TestBed.inject(VariableCategoryService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call VariableScope query and add missing value', () => { + const emissionFactor: IEmissionFactor = { id: 456 }; + const variableScope: IVariableScope = { id: 17355 }; + emissionFactor.variableScope = variableScope; + + const variableScopeCollection: IVariableScope[] = [{ id: 28014 }]; + jest.spyOn(variableScopeService, 'query').mockReturnValue(of(new HttpResponse({ body: variableScopeCollection }))); + const additionalVariableScopes = [variableScope]; + const expectedCollection: IVariableScope[] = [...additionalVariableScopes, ...variableScopeCollection]; + jest.spyOn(variableScopeService, 'addVariableScopeToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ emissionFactor }); + comp.ngOnInit(); + + expect(variableScopeService.query).toHaveBeenCalled(); + expect(variableScopeService.addVariableScopeToCollectionIfMissing).toHaveBeenCalledWith( + variableScopeCollection, + ...additionalVariableScopes.map(expect.objectContaining), + ); + expect(comp.variableScopesSharedCollection).toEqual(expectedCollection); + }); + + it('Should call VariableCategory query and add missing value', () => { + const emissionFactor: IEmissionFactor = { id: 456 }; + const variableCategory: IVariableCategory = { id: 29593 }; + emissionFactor.variableCategory = variableCategory; + + const variableCategoryCollection: IVariableCategory[] = [{ id: 12963 }]; + jest.spyOn(variableCategoryService, 'query').mockReturnValue(of(new HttpResponse({ body: variableCategoryCollection }))); + const additionalVariableCategories = [variableCategory]; + const expectedCollection: IVariableCategory[] = [...additionalVariableCategories, ...variableCategoryCollection]; + jest.spyOn(variableCategoryService, 'addVariableCategoryToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ emissionFactor }); + comp.ngOnInit(); + + expect(variableCategoryService.query).toHaveBeenCalled(); + expect(variableCategoryService.addVariableCategoryToCollectionIfMissing).toHaveBeenCalledWith( + variableCategoryCollection, + ...additionalVariableCategories.map(expect.objectContaining), + ); + expect(comp.variableCategoriesSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const emissionFactor: IEmissionFactor = { id: 456 }; + const variableScope: IVariableScope = { id: 17239 }; + emissionFactor.variableScope = variableScope; + const variableCategory: IVariableCategory = { id: 21647 }; + emissionFactor.variableCategory = variableCategory; + + activatedRoute.data = of({ emissionFactor }); + comp.ngOnInit(); + + expect(comp.variableScopesSharedCollection).toContain(variableScope); + expect(comp.variableCategoriesSharedCollection).toContain(variableCategory); + expect(comp.emissionFactor).toEqual(emissionFactor); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const emissionFactor = { id: 123 }; + jest.spyOn(emissionFactorFormService, 'getEmissionFactor').mockReturnValue(emissionFactor); + jest.spyOn(emissionFactorService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ emissionFactor }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: emissionFactor })); + saveSubject.complete(); + + // THEN + expect(emissionFactorFormService.getEmissionFactor).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(emissionFactorService.update).toHaveBeenCalledWith(expect.objectContaining(emissionFactor)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const emissionFactor = { id: 123 }; + jest.spyOn(emissionFactorFormService, 'getEmissionFactor').mockReturnValue({ id: null }); + jest.spyOn(emissionFactorService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ emissionFactor: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: emissionFactor })); + saveSubject.complete(); + + // THEN + expect(emissionFactorFormService.getEmissionFactor).toHaveBeenCalled(); + expect(emissionFactorService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const emissionFactor = { id: 123 }; + jest.spyOn(emissionFactorService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ emissionFactor }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(emissionFactorService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareVariableScope', () => { + it('Should forward to variableScopeService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(variableScopeService, 'compareVariableScope'); + comp.compareVariableScope(entity, entity2); + expect(variableScopeService.compareVariableScope).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareVariableCategory', () => { + it('Should forward to variableCategoryService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(variableCategoryService, 'compareVariableCategory'); + comp.compareVariableCategory(entity, entity2); + expect(variableCategoryService.compareVariableCategory).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.ts b/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.ts new file mode 100644 index 0000000..bae88af --- /dev/null +++ b/src/main/webapp/app/entities/emission-factor/update/emission-factor-update.component.ts @@ -0,0 +1,158 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, tap, of } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { EmissionFactorService } from '../service/emission-factor.service'; +import { IEmissionFactor } from '../emission-factor.model'; +import { EmissionFactorFormService, EmissionFactorFormGroup } from './emission-factor-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-emission-factor-update', + templateUrl: './emission-factor-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class EmissionFactorUpdateComponent implements OnInit { + isSaving = false; + emissionFactor: IEmissionFactor | null = null; + + variableScopesSharedCollection: IVariableScope[] = []; + variableCategoriesSharedCollection: IVariableCategory[] = []; + + protected emissionFactorService = inject(EmissionFactorService); + protected emissionFactorFormService = inject(EmissionFactorFormService); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: EmissionFactorFormGroup = this.emissionFactorFormService.createEmissionFactorFormGroup(); + + compareVariableScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + compareVariableCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ emissionFactor }) => { + this.emissionFactor = emissionFactor; + if (emissionFactor) { + // Is cloning ? + const lastSegment = this.activatedRoute.snapshot.url[this.activatedRoute.snapshot.url.length - 1]?.path; + const isCloning = lastSegment === 'clone'; + if (isCloning) { + // Clear the ID and Version + emissionFactor.id = null; + emissionFactor.version = null; + } + + this.updateForm(emissionFactor); + } + + // Listen to changes to the scope filter property + this.editForm.get('variableScope')?.valueChanges.subscribe((scope) => { + this.editForm.get('variableCategory')?.setValue(null); + this.loadCategoriesOfSelectedScope(scope).subscribe((cats: IVariableCategory[] | null) => { + if (cats) { + this.variableCategoriesSharedCollection = cats; + } + }); + }); + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const emissionFactor = this.emissionFactorFormService.getEmissionFactor(this.editForm); + if (emissionFactor.id !== null) { + this.subscribeToSaveResponse(this.emissionFactorService.update(emissionFactor)); + } else { + this.subscribeToSaveResponse(this.emissionFactorService.create(emissionFactor)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(emissionFactor: IEmissionFactor): void { + this.emissionFactor = emissionFactor; + this.emissionFactorFormService.resetForm(this.editForm, emissionFactor); + + this.variableScopesSharedCollection = this.variableScopeService.addEntityToCollectionIfMissing( + this.variableScopesSharedCollection, + emissionFactor.variableScope, + ); + + this.loadCategoriesOfSelectedScope(emissionFactor.variableScope).subscribe((cats: IVariableCategory[] | null) => { + if (cats) { + this.variableCategoriesSharedCollection = cats; + } + + this.variableCategoriesSharedCollection = this.variableCategoryService.addEntityToCollectionIfMissing( + this.variableCategoriesSharedCollection, + emissionFactor.variableCategory, + ); + }); + } + + protected loadCategoriesOfSelectedScope(scope: IVariableScope|null|undefined): Observable { + if (scope) { + return this.variableCategoryService + .queryByScopeForFactor(scope.id) + .pipe(map((res: HttpResponse) => res.body ?? [])); + } else { + return of(null).pipe( + tap(() => { + this.variableCategoriesSharedCollection = []; + }) + ); + } + } + + protected loadRelationshipsOptions(): void { + this.variableScopeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((variableScopes: IVariableScope[]) => + this.variableScopeService.addEntityToCollectionIfMissing( + variableScopes, + this.emissionFactor?.variableScope, + ), + ), + ) + .subscribe((variableScopes: IVariableScope[]) => (this.variableScopesSharedCollection = variableScopes)); + } +} diff --git a/src/main/webapp/app/entities/entity-navbar-items.ts b/src/main/webapp/app/entities/entity-navbar-items.ts new file mode 100644 index 0000000..cf204d0 --- /dev/null +++ b/src/main/webapp/app/entities/entity-navbar-items.ts @@ -0,0 +1,99 @@ +import NavbarItem from 'app/layouts/navbar/navbar-item.model'; + +export const EntityNavbarItems: NavbarItem[] = [ + { + name: 'OrganizationType', + route: '/organization-type', + translationKey: 'global.menu.entities.organizationType', + }, + { + name: 'Organization', + route: '/organization', + translationKey: 'global.menu.entities.organization', + }, + { + name: 'MetadataProperty', + route: '/metadata-property', + translationKey: 'global.menu.entities.metadataProperty', + }, + { + name: 'MetadataValue', + route: '/metadata-value', + translationKey: 'global.menu.entities.metadataValue', + }, + { + name: 'UnitType', + route: '/unit-type', + translationKey: 'global.menu.entities.unitType', + }, + { + name: 'Unit', + route: '/unit', + translationKey: 'global.menu.entities.unit', + }, + { + name: 'UnitConverter', + route: '/unit-converter', + translationKey: 'global.menu.entities.unitConverter', + }, + { + name: 'VariableScope', + route: '/variable-scope', + translationKey: 'global.menu.entities.variableScope', + }, + { + name: 'VariableCategory', + route: '/variable-category', + translationKey: 'global.menu.entities.variableCategory', + }, + { + name: 'Variable', + route: '/variable', + translationKey: 'global.menu.entities.variable', + }, + { + name: 'VariableUnits', + route: '/variable-units', + translationKey: 'global.menu.entities.variableUnits', + }, + { + name: 'VariableClass', + route: '/variable-class', + translationKey: 'global.menu.entities.variableClass', + }, + { + name: 'VariableClassType', + route: '/variable-class-type', + translationKey: 'global.menu.entities.variableClassType', + }, + { + name: 'Period', + route: '/period', + translationKey: 'global.menu.entities.period', + }, + { + name: 'PeriodVersion', + route: '/period-version', + translationKey: 'global.menu.entities.periodVersion', + }, + { + name: 'InputData', + route: '/input-data', + translationKey: 'global.menu.entities.inputData', + }, + { + name: 'InputDataUpload', + route: '/input-data-upload', + translationKey: 'global.menu.entities.inputDataUpload', + }, + { + name: 'InputDataUploadLog', + route: '/input-data-upload-log', + translationKey: 'global.menu.entities.inputDataUploadLog', + }, + { + name: 'OutputData', + route: '/output-data', + translationKey: 'global.menu.entities.outputData', + }, +]; diff --git a/src/main/webapp/app/entities/entity.routes.ts b/src/main/webapp/app/entities/entity.routes.ts new file mode 100644 index 0000000..ecf24e4 --- /dev/null +++ b/src/main/webapp/app/entities/entity.routes.ts @@ -0,0 +1,123 @@ +import { Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'authority', + data: { pageTitle: 'resilientApp.adminAuthority.home.title' }, + loadChildren: () => import('./admin/authority/authority.routes'), + }, + { + path: 'organization-type', + data: { pageTitle: 'resilientApp.organizationType.home.title' }, + loadChildren: () => import('./organization-type/organization-type.routes'), + }, + { + path: 'organization', + data: { pageTitle: 'resilientApp.organization.home.title' }, + loadChildren: () => import('./organization/organization.routes'), + }, + { + path: 'metadata-property', + data: { pageTitle: 'resilientApp.metadataProperty.home.title' }, + loadChildren: () => import('./metadata-property/metadata-property.routes'), + }, + { + path: 'metadata-value', + data: { pageTitle: 'resilientApp.metadataValue.home.title' }, + loadChildren: () => import('./metadata-value/metadata-value.routes'), + }, + { + path: 'unit-type', + data: { pageTitle: 'resilientApp.unitType.home.title' }, + loadChildren: () => import('./unit-type/unit-type.routes'), + }, + { + path: 'unit', + data: { pageTitle: 'resilientApp.unit.home.title' }, + loadChildren: () => import('./unit/unit.routes'), + }, + { + path: 'unit-converter', + data: { pageTitle: 'resilientApp.unitConverter.home.title' }, + loadChildren: () => import('./unit-converter/unit-converter.routes'), + }, + { + path: 'variable-scope', + data: { pageTitle: 'resilientApp.variableScope.home.title' }, + loadChildren: () => import('./variable-scope/variable-scope.routes'), + }, + { + path: 'variable-category', + data: { pageTitle: 'resilientApp.variableCategory.home.title' }, + loadChildren: () => import('./variable-category/variable-category.routes'), + }, + { + path: 'variable', + data: { pageTitle: 'resilientApp.variable.home.title' }, + loadChildren: () => import('./variable/variable.routes'), + }, + /* REMOVED 'variable-units'. No direct access. Maintenance done from 'variable' */ + { + path: 'variable-class-type', + data: { pageTitle: 'resilientApp.variableClassType.home.title' }, + loadChildren: () => import('./variable-class-type/variable-class-type.routes'), + }, + /* REMOVED 'variable-class'. No direct access. Maintenance done from 'variable-class-type' */ + { + path: 'period', + data: { pageTitle: 'resilientApp.period.home.title' }, + loadChildren: () => import('./period/period.routes'), + }, + { + path: 'emission-factor', + data: { pageTitle: 'resilientApp.emissionFactor.home.title' }, + loadChildren: () => import('./emission-factor/emission-factor.routes'), + }, + { + path: 'input-data', + data: { pageTitle: 'resilientApp.inputData.home.title' }, + loadChildren: () => import('./input-data/input-data.routes'), + }, + { + path: 'input-data-upload', + data: { pageTitle: 'resilientApp.inputDataUpload.home.title' }, + loadChildren: () => import('./input-data-upload/input-data-upload.routes'), + }, + { + path: 'input-data-upload-log', + data: { pageTitle: 'resilientApp.inputDataUploadLog.home.title' }, + loadChildren: () => import('./input-data-upload-log/input-data-upload-log.routes'), + }, + { + path: 'output-data', + data: { pageTitle: 'resilientApp.outputData.home.title' }, + loadChildren: () => import('./output-data/output-data.routes'), + }, + { + path: 'dashboard-component', + data: { pageTitle: 'resilientApp.dashboardComponent.home.title' }, + loadChildren: () => import('./dashboard-component/dashboard-component.routes'), + }, + { + path: 'dashboard', + data: { pageTitle: 'resilientApp.dashboard.home.title' }, + loadChildren: () => import('./dashboard/dashboard.routes'), + }, + { + path: 'inventory', + data: { pageTitle: 'resilientApp.inventory.home.title' }, + loadChildren: () => import('./inventory/inventory.routes'), + }, + { + path: 'document', + data: { pageTitle: 'resilientApp.document.home.title' }, + loadChildren: () => import('./document/document.routes'), + }, + { + path: 'content-page', + data: { pageTitle: 'resilientApp.contentPage.home.title' }, + loadChildren: () => import('./content-page/content-page.routes'), + }, +]; + +export default routes; diff --git a/src/main/webapp/app/entities/enumerations/content-page-type.model.ts b/src/main/webapp/app/entities/enumerations/content-page-type.model.ts new file mode 100644 index 0000000..ef21a91 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/content-page-type.model.ts @@ -0,0 +1,4 @@ +export enum ContentPageType { + HOME_ANONYMOUS = 'HOME_ANONYMOUS', + HOME_AUTHENTICATED = 'HOME_AUTHENTICATED', +} diff --git a/src/main/webapp/app/entities/enumerations/dashboard-component-chart-type.model.ts b/src/main/webapp/app/entities/enumerations/dashboard-component-chart-type.model.ts new file mode 100644 index 0000000..f0b283c --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/dashboard-component-chart-type.model.ts @@ -0,0 +1,5 @@ +export enum DashboardComponentChartType { + BAR = 'BAR', + + PIE = 'PIE', +} diff --git a/src/main/webapp/app/entities/enumerations/dashboard-component-type.model.ts b/src/main/webapp/app/entities/enumerations/dashboard-component-type.model.ts new file mode 100644 index 0000000..fc3be90 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/dashboard-component-type.model.ts @@ -0,0 +1,7 @@ +export enum DashboardComponentType { + TABLE = 'TABLE', + + ACCORDION = 'ACCORDION', + + CHART = 'CHART', +} diff --git a/src/main/webapp/app/entities/enumerations/dashboard-component-view.model.ts b/src/main/webapp/app/entities/enumerations/dashboard-component-view.model.ts new file mode 100644 index 0000000..20c8009 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/dashboard-component-view.model.ts @@ -0,0 +1,5 @@ +export enum DashboardComponentView { + EMISSIONS = 'EMISSIONS', + + INDICATORS = 'INDICATORS', +} diff --git a/src/main/webapp/app/entities/enumerations/data-source-type.model.ts b/src/main/webapp/app/entities/enumerations/data-source-type.model.ts new file mode 100644 index 0000000..6a9b38e --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/data-source-type.model.ts @@ -0,0 +1,9 @@ +export enum DataSourceType { + MANUAL = 'MANUAL', + + FILE = 'FILE', + + AUTO = 'AUTO', + + DISTRIB = 'DISTRIB', +} diff --git a/src/main/webapp/app/entities/enumerations/input-mode.model.ts b/src/main/webapp/app/entities/enumerations/input-mode.model.ts new file mode 100644 index 0000000..03908b4 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/input-mode.model.ts @@ -0,0 +1,9 @@ +export enum InputMode { + DATAFILE = 'DATAFILE', + + MANUAL = 'MANUAL', + + INTEGRATION = 'INTEGRATION', + + ANY = 'ANY', +} diff --git a/src/main/webapp/app/entities/enumerations/metadata-type.model.ts b/src/main/webapp/app/entities/enumerations/metadata-type.model.ts new file mode 100644 index 0000000..0cc5f12 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/metadata-type.model.ts @@ -0,0 +1,11 @@ +export enum MetadataType { + STRING = 'STRING', + + BOOLEAN = 'BOOLEAN', + + INTEGER = 'INTEGER', + + DECIMAL = 'DECIMAL', + + DATE = 'DATE', +} diff --git a/src/main/webapp/app/entities/enumerations/organization-nature.model.ts b/src/main/webapp/app/entities/enumerations/organization-nature.model.ts new file mode 100644 index 0000000..d1fcc7f --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/organization-nature.model.ts @@ -0,0 +1,9 @@ +export enum OrganizationNature { + ORGANIZATION = 'ORGANIZATION', + + PERSON = 'PERSON', + + FACILITY = 'FACILITY', + + LEVEL = 'LEVEL', +} diff --git a/src/main/webapp/app/entities/enumerations/period-status.model.ts b/src/main/webapp/app/entities/enumerations/period-status.model.ts new file mode 100644 index 0000000..ce939fd --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/period-status.model.ts @@ -0,0 +1,7 @@ +export enum PeriodStatus { + CREATED = 'CREATED', + OPENED = 'OPENED', + CLOSED = 'CLOSED', + PROCESSING = 'PROCESSING', + ENDED = 'ENDED', +} diff --git a/src/main/webapp/app/entities/enumerations/period-version-status.model.ts b/src/main/webapp/app/entities/enumerations/period-version-status.model.ts new file mode 100644 index 0000000..3f49518 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/period-version-status.model.ts @@ -0,0 +1,5 @@ +export enum PeriodVersionStatus { + PROCESSING = 'PROCESSING', + ENDED = 'ENDED', + ERROR = 'ERROR', +} diff --git a/src/main/webapp/app/entities/enumerations/resilient-log-level.model.ts b/src/main/webapp/app/entities/enumerations/resilient-log-level.model.ts new file mode 100644 index 0000000..456d158 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/resilient-log-level.model.ts @@ -0,0 +1,5 @@ +export enum ResilientLogLevel { + INFO = 'INFO', + WARN = 'WARN', + ERROR = 'ERROR' +} diff --git a/src/main/webapp/app/entities/enumerations/unit-value-type.model.ts b/src/main/webapp/app/entities/enumerations/unit-value-type.model.ts new file mode 100644 index 0000000..c8147cb --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/unit-value-type.model.ts @@ -0,0 +1,7 @@ +export enum UnitValueType { + DECIMAL = 'DECIMAL', + + BOOLEAN = 'BOOLEAN', + + STRING = 'STRING', +} diff --git a/src/main/webapp/app/entities/enumerations/upload-status.model.ts b/src/main/webapp/app/entities/enumerations/upload-status.model.ts new file mode 100644 index 0000000..075e623 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/upload-status.model.ts @@ -0,0 +1,9 @@ +export enum UploadStatus { + UPLOADED = 'UPLOADED', + + PROCESSING = 'PROCESSING', + + PROCESSED = 'PROCESSED', + + ERROR = 'ERROR', +} diff --git a/src/main/webapp/app/entities/enumerations/upload-type.model.ts b/src/main/webapp/app/entities/enumerations/upload-type.model.ts new file mode 100644 index 0000000..f60b999 --- /dev/null +++ b/src/main/webapp/app/entities/enumerations/upload-type.model.ts @@ -0,0 +1,7 @@ +export enum UploadType { + INVENTORY = 'INVENTORY', + ENERGY = 'ENERGY', + GAS = 'GAS', + GLOBAL = 'GLOBAL', + ACCOUNTING = 'ACCOUNTING', +} diff --git a/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.html b/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.html new file mode 100644 index 0000000..5f530c5 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (inputDataUploadLog) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.spec.ts new file mode 100644 index 0000000..3e9973e --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; + +import { InputDataUploadLogDeleteDialogComponent } from './input-data-upload-log-delete-dialog.component'; + +describe('InputDataUploadLog Management Delete Component', () => { + let comp: InputDataUploadLogDeleteDialogComponent; + let fixture: ComponentFixture; + let service: InputDataUploadLogService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadLogDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(InputDataUploadLogDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(InputDataUploadLogDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataUploadLogService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.ts b/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.ts new file mode 100644 index 0000000..8fc8958 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/delete/input-data-upload-log-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; + +@Component({ + standalone: true, + templateUrl: './input-data-upload-log-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class InputDataUploadLogDeleteDialogComponent { + inputDataUploadLog?: IInputDataUploadLog; + + protected inputDataUploadLogService = inject(InputDataUploadLogService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.inputDataUploadLogService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.html b/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.html new file mode 100644 index 0000000..42170f8 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.html @@ -0,0 +1,82 @@ +
+
+ @if (inputDataUploadLog()) { +
+

+ Input Data Upload Log +

+ +
+ + + + + +
+
Código
+
+ {{ inputDataUploadLog()!.id }} +
+
+ Log Message +
+
+ {{ inputDataUploadLog()!.logMessage }} +
+
+ Creation Date +
+
+ {{ inputDataUploadLog()!.creationDate | formatMediumDatetime }} +
+
+ Creation Username +
+
+ {{ inputDataUploadLog()!.creationUsername }} +
+
+ Version +
+
+ {{ inputDataUploadLog()!.version }} +
+
Input Data Upload
+
+ @if (inputDataUploadLog()!.inputDataUpload) { + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.spec.ts new file mode 100644 index 0000000..5b15198 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { InputDataUploadLogDetailComponent } from './input-data-upload-log-detail.component'; + +describe('InputDataUploadLog Management Detail Component', () => { + let comp: InputDataUploadLogDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InputDataUploadLogDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: InputDataUploadLogDetailComponent, + resolve: { inputDataUploadLog: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(InputDataUploadLogDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InputDataUploadLogDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load inputDataUploadLog on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', InputDataUploadLogDetailComponent); + + // THEN + expect(instance.inputDataUploadLog()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.ts b/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.ts new file mode 100644 index 0000000..adb4032 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/detail/input-data-upload-log-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IInputDataUploadLog } from '../input-data-upload-log.model'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-upload-log-detail', + templateUrl: './input-data-upload-log-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class InputDataUploadLogDetailComponent { + inputDataUploadLog = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.model.ts b/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.model.ts new file mode 100644 index 0000000..be8b716 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.model.ts @@ -0,0 +1,13 @@ +import dayjs from 'dayjs/esm'; +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IInputDataUpload } from 'app/entities/input-data-upload/input-data-upload.model'; + +export interface IInputDataUploadLog extends IResilientEntity { + logMessage?: string | null; + creationDate?: dayjs.Dayjs | null; + creationUsername?: string | null; + inputDataUpload?: Pick | null; +} + +export type NewInputDataUploadLog = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.routes.ts b/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.routes.ts new file mode 100644 index 0000000..e50119d --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.routes.ts @@ -0,0 +1,42 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { InputDataUploadLogComponent } from './list/input-data-upload-log.component'; +import { InputDataUploadLogDetailComponent } from './detail/input-data-upload-log-detail.component'; +import { InputDataUploadLogUpdateComponent } from './update/input-data-upload-log-update.component'; +import InputDataUploadLogResolve from './route/input-data-upload-log-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; +import { InputDataUploadLogService } from './service/input-data-upload-log.service'; + +const inputDataUploadLogRoute: Routes = [ + { + path: '', + component: InputDataUploadLogComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: InputDataUploadLogService, + resource: Resources.INPUT_DATA_UPLOAD_LOG, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: ':id/view', + component: InputDataUploadLogDetailComponent, + resolve: { + inputDataUploadLog: InputDataUploadLogResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: InputDataUploadLogService, + resource: Resources.INPUT_DATA_UPLOAD_LOG, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, +]; + +export default inputDataUploadLogRoute; diff --git a/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.test-samples.ts b/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.test-samples.ts new file mode 100644 index 0000000..51ed1a6 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/input-data-upload-log.test-samples.ts @@ -0,0 +1,38 @@ +import dayjs from 'dayjs/esm'; + +import { IInputDataUploadLog, NewInputDataUploadLog } from './input-data-upload-log.model'; + +export const sampleWithRequiredData: IInputDataUploadLog = { + id: 3266, + logMessage: 'after incidentally cute', + creationDate: dayjs('2024-10-14T21:07'), + creationUsername: 'like leveret constant', +}; + +export const sampleWithPartialData: IInputDataUploadLog = { + id: 7937, + logMessage: 'to jeweller', + creationDate: dayjs('2024-10-15T04:40'), + creationUsername: 'familiar', + version: 19487, +}; + +export const sampleWithFullData: IInputDataUploadLog = { + id: 31573, + logMessage: 'phew well-worn', + creationDate: dayjs('2024-10-15T07:17'), + creationUsername: 'tragic brr oof', + version: 6115, +}; + +export const sampleWithNewData: NewInputDataUploadLog = { + logMessage: 'hmph spend', + creationDate: dayjs('2024-10-14T19:31'), + creationUsername: 'loft sheepishly', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.html b/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.html new file mode 100644 index 0000000..42f5ce9 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.html @@ -0,0 +1,133 @@ +
+

+ Input Data Upload Logs + +
+ + + +
+

+ + + + + + @if (inputDataUploadLogs?.length === 0) { +
+ Nenhum Input Data Upload Logs encontrado +
+ } + + @if (inputDataUploadLogs && inputDataUploadLogs.length > 0) { +
+ + + + + + + + + + + + + + @for (inputDataUploadLog of inputDataUploadLogs; track trackId) { + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Log Message + + +
+
+
+ Creation Date + + +
+
+
+ Creation Username + + +
+
+
+ Version + + +
+
+
+ Input Data Upload + +
+
+ {{ inputDataUploadLog.id }} + {{ inputDataUploadLog.logMessage }}{{ inputDataUploadLog.creationDate | formatMediumDatetime }}{{ inputDataUploadLog.creationUsername }}{{ inputDataUploadLog.version }} + @if (inputDataUploadLog.inputDataUpload) { + + } + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.spec.ts new file mode 100644 index 0000000..2cf26b7 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../input-data-upload-log.test-samples'; +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; + +import { InputDataUploadLogComponent } from './input-data-upload-log.component'; +import SpyInstance = jest.SpyInstance; + +describe('InputDataUploadLog Management Component', () => { + let comp: InputDataUploadLogComponent; + let fixture: ComponentFixture; + let service: InputDataUploadLogService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadLogComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(InputDataUploadLogComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(InputDataUploadLogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataUploadLogService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.inputDataUploadLogs?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to inputDataUploadLogService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getInputDataUploadLogIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getInputDataUploadLogIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.ts b/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.ts new file mode 100644 index 0000000..578a5b7 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/list/input-data-upload-log.component.ts @@ -0,0 +1,122 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { EntityArrayResponseType, InputDataUploadLogService } from '../service/input-data-upload-log.service'; +import { InputDataUploadLogDeleteDialogComponent } from '../delete/input-data-upload-log-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-upload-log', + templateUrl: './input-data-upload-log.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class InputDataUploadLogComponent implements OnInit { + subscription: Subscription | null = null; + inputDataUploadLogs?: IInputDataUploadLog[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected inputDataUploadLogService = inject(InputDataUploadLogService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IInputDataUploadLog): number => this.inputDataUploadLogService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.inputDataUploadLogs || this.inputDataUploadLogs.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(inputDataUploadLog: IInputDataUploadLog): void { + const modalRef = this.modalService.open(InputDataUploadLogDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.inputDataUploadLog = inputDataUploadLog; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.inputDataUploadLogs = this.refineData(dataFromBody); + } + + protected refineData(data: IInputDataUploadLog[]): IInputDataUploadLog[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IInputDataUploadLog[] | null): IInputDataUploadLog[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.inputDataUploadLogService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload-log/route/input-data-upload-log-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/route/input-data-upload-log-routing-resolve.service.spec.ts new file mode 100644 index 0000000..3b9c2d8 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/route/input-data-upload-log-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; + +import inputDataUploadLogResolve from './input-data-upload-log-routing-resolve.service'; + +describe('InputDataUploadLog routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: InputDataUploadLogService; + let resultInputDataUploadLog: IInputDataUploadLog | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(InputDataUploadLogService); + resultInputDataUploadLog = undefined; + }); + + describe('resolve', () => { + it('should return IInputDataUploadLog returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataUploadLogResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputDataUploadLog = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultInputDataUploadLog).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataUploadLogResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputDataUploadLog = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultInputDataUploadLog).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataUploadLogResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputDataUploadLog = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultInputDataUploadLog).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/route/input-data-upload-log-routing-resolve.service.ts b/src/main/webapp/app/entities/input-data-upload-log/route/input-data-upload-log-routing-resolve.service.ts new file mode 100644 index 0000000..77c0ef5 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/route/input-data-upload-log-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; + +const inputDataUploadLogResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(InputDataUploadLogService) + .find(id) + .pipe( + mergeMap((inputDataUploadLog: HttpResponse) => { + if (inputDataUploadLog.body) { + return of(inputDataUploadLog.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default inputDataUploadLogResolve; diff --git a/src/main/webapp/app/entities/input-data-upload-log/service/input-data-upload-log.service.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/service/input-data-upload-log.service.spec.ts new file mode 100644 index 0000000..a1f1d55 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/service/input-data-upload-log.service.spec.ts @@ -0,0 +1,210 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { + sampleWithRequiredData, + sampleWithNewData, + sampleWithPartialData, + sampleWithFullData, +} from '../input-data-upload-log.test-samples'; + +import { InputDataUploadLogService, RestInputDataUploadLog } from './input-data-upload-log.service'; + +const requireRestSample: RestInputDataUploadLog = { + ...sampleWithRequiredData, + creationDate: sampleWithRequiredData.creationDate?.toJSON(), +}; + +describe('InputDataUploadLog Service', () => { + let service: InputDataUploadLogService; + let httpMock: HttpTestingController; + let expectedResult: IInputDataUploadLog | IInputDataUploadLog[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(InputDataUploadLogService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a InputDataUploadLog', () => { + const inputDataUploadLog = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(inputDataUploadLog).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a InputDataUploadLog', () => { + const inputDataUploadLog = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(inputDataUploadLog).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a InputDataUploadLog', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of InputDataUploadLog', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a InputDataUploadLog', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addInputDataUploadLogToCollectionIfMissing', () => { + it('should add a InputDataUploadLog to an empty array', () => { + const inputDataUploadLog: IInputDataUploadLog = sampleWithRequiredData; + expectedResult = service.addInputDataUploadLogToCollectionIfMissing([], inputDataUploadLog); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(inputDataUploadLog); + }); + + it('should not add a InputDataUploadLog to an array that contains it', () => { + const inputDataUploadLog: IInputDataUploadLog = sampleWithRequiredData; + const inputDataUploadLogCollection: IInputDataUploadLog[] = [ + { + ...inputDataUploadLog, + }, + sampleWithPartialData, + ]; + expectedResult = service.addInputDataUploadLogToCollectionIfMissing(inputDataUploadLogCollection, inputDataUploadLog); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a InputDataUploadLog to an array that doesn't contain it", () => { + const inputDataUploadLog: IInputDataUploadLog = sampleWithRequiredData; + const inputDataUploadLogCollection: IInputDataUploadLog[] = [sampleWithPartialData]; + expectedResult = service.addInputDataUploadLogToCollectionIfMissing(inputDataUploadLogCollection, inputDataUploadLog); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(inputDataUploadLog); + }); + + it('should add only unique InputDataUploadLog to an array', () => { + const inputDataUploadLogArray: IInputDataUploadLog[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const inputDataUploadLogCollection: IInputDataUploadLog[] = [sampleWithRequiredData]; + expectedResult = service.addInputDataUploadLogToCollectionIfMissing(inputDataUploadLogCollection, ...inputDataUploadLogArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const inputDataUploadLog: IInputDataUploadLog = sampleWithRequiredData; + const inputDataUploadLog2: IInputDataUploadLog = sampleWithPartialData; + expectedResult = service.addInputDataUploadLogToCollectionIfMissing([], inputDataUploadLog, inputDataUploadLog2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(inputDataUploadLog); + expect(expectedResult).toContain(inputDataUploadLog2); + }); + + it('should accept null and undefined values', () => { + const inputDataUploadLog: IInputDataUploadLog = sampleWithRequiredData; + expectedResult = service.addInputDataUploadLogToCollectionIfMissing([], null, inputDataUploadLog, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(inputDataUploadLog); + }); + + it('should return initial array if no InputDataUploadLog is added', () => { + const inputDataUploadLogCollection: IInputDataUploadLog[] = [sampleWithRequiredData]; + expectedResult = service.addInputDataUploadLogToCollectionIfMissing(inputDataUploadLogCollection, undefined, null); + expect(expectedResult).toEqual(inputDataUploadLogCollection); + }); + }); + + describe('compareInputDataUploadLog', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareInputDataUploadLog(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareInputDataUploadLog(entity1, entity2); + const compareResult2 = service.compareInputDataUploadLog(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareInputDataUploadLog(entity1, entity2); + const compareResult2 = service.compareInputDataUploadLog(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareInputDataUploadLog(entity1, entity2); + const compareResult2 = service.compareInputDataUploadLog(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/service/input-data-upload-log.service.ts b/src/main/webapp/app/entities/input-data-upload-log/service/input-data-upload-log.service.ts new file mode 100644 index 0000000..b4797d7 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/service/input-data-upload-log.service.ts @@ -0,0 +1,35 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum, RestOf } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IInputDataUploadLog, NewInputDataUploadLog } from '../input-data-upload-log.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class InputDataUploadLogService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/input-data-upload-logs'); + override getResourceUrl(): string { return this.resourceUrl; } + + private readonly dateProperties: Map = new Map([ + ["creationDate", DateTypeEnum.TIMESTAMP] + ]); + override getDateProperties(): Map { return this.dateProperties; } + + + findByOwner(id: number): Observable { + return this.http + .get[]>(`${this.resourceUrl}/owner/${id}`, { observe: 'response' }) + .pipe(map(res => this.convertResponseArrayFromServer(res))); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-form.service.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-form.service.spec.ts new file mode 100644 index 0000000..8560e43 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-form.service.spec.ts @@ -0,0 +1,94 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../input-data-upload-log.test-samples'; + +import { InputDataUploadLogFormService } from './input-data-upload-log-form.service'; + +describe('InputDataUploadLog Form Service', () => { + let service: InputDataUploadLogFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(InputDataUploadLogFormService); + }); + + describe('Service methods', () => { + describe('createInputDataUploadLogFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createInputDataUploadLogFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + logMessage: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + inputDataUpload: expect.any(Object), + }), + ); + }); + + it('passing IInputDataUploadLog should create a new form with FormGroup', () => { + const formGroup = service.createInputDataUploadLogFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + logMessage: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + inputDataUpload: expect.any(Object), + }), + ); + }); + }); + + describe('getInputDataUploadLog', () => { + it('should return NewInputDataUploadLog for default InputDataUploadLog initial value', () => { + const formGroup = service.createInputDataUploadLogFormGroup(sampleWithNewData); + + const inputDataUploadLog = service.getInputDataUploadLog(formGroup) as any; + + expect(inputDataUploadLog).toMatchObject(sampleWithNewData); + }); + + it('should return NewInputDataUploadLog for empty InputDataUploadLog initial value', () => { + const formGroup = service.createInputDataUploadLogFormGroup(); + + const inputDataUploadLog = service.getInputDataUploadLog(formGroup) as any; + + expect(inputDataUploadLog).toMatchObject({}); + }); + + it('should return IInputDataUploadLog', () => { + const formGroup = service.createInputDataUploadLogFormGroup(sampleWithRequiredData); + + const inputDataUploadLog = service.getInputDataUploadLog(formGroup) as any; + + expect(inputDataUploadLog).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IInputDataUploadLog should not enable id FormControl', () => { + const formGroup = service.createInputDataUploadLogFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewInputDataUploadLog should disable id FormControl', () => { + const formGroup = service.createInputDataUploadLogFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-form.service.ts b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-form.service.ts new file mode 100644 index 0000000..095914d --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-form.service.ts @@ -0,0 +1,117 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { IInputDataUploadLog, NewInputDataUploadLog } from '../input-data-upload-log.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IInputDataUploadLog for edit and NewInputDataUploadLogFormGroupInput for create. + */ +type InputDataUploadLogFormGroupInput = IInputDataUploadLog | PartialWithRequiredKeyOf; + +/** + * Type that converts some properties for forms. + */ +type FormValueOf = Omit & { + creationDate?: string | null; +}; + +type InputDataUploadLogFormRawValue = FormValueOf; + +type NewInputDataUploadLogFormRawValue = FormValueOf; + +type InputDataUploadLogFormDefaults = Pick; + +type InputDataUploadLogFormGroupContent = { + id: FormControl; + logMessage: FormControl; + creationDate: FormControl; + creationUsername: FormControl; + version: FormControl; + inputDataUpload: FormControl; +}; + +export type InputDataUploadLogFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class InputDataUploadLogFormService { + createInputDataUploadLogFormGroup(inputDataUploadLog: InputDataUploadLogFormGroupInput = { id: null }): InputDataUploadLogFormGroup { + const inputDataUploadLogRawValue = this.convertInputDataUploadLogToInputDataUploadLogRawValue({ + ...this.getFormDefaults(), + ...inputDataUploadLog, + }); + return new FormGroup({ + id: new FormControl( + { value: inputDataUploadLogRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + logMessage: new FormControl(inputDataUploadLogRawValue.logMessage, { + validators: [Validators.required], + }), + creationDate: new FormControl(inputDataUploadLogRawValue.creationDate, { + validators: [Validators.required], + }), + creationUsername: new FormControl(inputDataUploadLogRawValue.creationUsername, { + validators: [Validators.required], + }), + version: new FormControl(inputDataUploadLogRawValue.version), + inputDataUpload: new FormControl(inputDataUploadLogRawValue.inputDataUpload), + }); + } + + getInputDataUploadLog(form: InputDataUploadLogFormGroup): IInputDataUploadLog | NewInputDataUploadLog { + return this.convertInputDataUploadLogRawValueToInputDataUploadLog( + form.getRawValue() as InputDataUploadLogFormRawValue | NewInputDataUploadLogFormRawValue, + ); + } + + resetForm(form: InputDataUploadLogFormGroup, inputDataUploadLog: InputDataUploadLogFormGroupInput): void { + const inputDataUploadLogRawValue = this.convertInputDataUploadLogToInputDataUploadLogRawValue({ + ...this.getFormDefaults(), + ...inputDataUploadLog, + }); + form.reset( + { + ...inputDataUploadLogRawValue, + id: { value: inputDataUploadLogRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): InputDataUploadLogFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + creationDate: currentTime, + }; + } + + private convertInputDataUploadLogRawValueToInputDataUploadLog( + rawInputDataUploadLog: InputDataUploadLogFormRawValue | NewInputDataUploadLogFormRawValue, + ): IInputDataUploadLog | NewInputDataUploadLog { + return { + ...rawInputDataUploadLog, + creationDate: dayjs(rawInputDataUploadLog.creationDate, DATE_TIME_FORMAT), + }; + } + + private convertInputDataUploadLogToInputDataUploadLogRawValue( + inputDataUploadLog: IInputDataUploadLog | (Partial & InputDataUploadLogFormDefaults), + ): InputDataUploadLogFormRawValue | PartialWithRequiredKeyOf { + return { + ...inputDataUploadLog, + creationDate: inputDataUploadLog.creationDate ? inputDataUploadLog.creationDate.format(DATE_TIME_FORMAT) : undefined, + }; + } +} diff --git a/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.html b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.html new file mode 100644 index 0000000..fcdbf3b --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.html @@ -0,0 +1,157 @@ +
+
+
+

+ Criar ou editar Input Data Upload Log +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if (editForm.get('logMessage')!.invalid && (editForm.get('logMessage')!.dirty || editForm.get('logMessage')!.touched)) { +
+ @if (editForm.get('logMessage')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ +
+ +
+ @if (editForm.get('creationDate')!.invalid && (editForm.get('creationDate')!.dirty || editForm.get('creationDate')!.touched)) { +
+ @if (editForm.get('creationDate')?.errors?.required) { + O campo é obrigatório. + } + Este campo é do tipo data/hora. +
+ } +
+ +
+ + + @if ( + editForm.get('creationUsername')!.invalid && + (editForm.get('creationUsername')!.dirty || editForm.get('creationUsername')!.touched) + ) { +
+ @if (editForm.get('creationUsername')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.spec.ts new file mode 100644 index 0000000..0dc7f9e --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.spec.ts @@ -0,0 +1,164 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IInputDataUpload } from 'app/entities/input-data-upload/input-data-upload.model'; +import { InputDataUploadService } from 'app/entities/input-data-upload/service/input-data-upload.service'; +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { InputDataUploadLogFormService } from './input-data-upload-log-form.service'; + +import { InputDataUploadLogUpdateComponent } from './input-data-upload-log-update.component'; + +describe('InputDataUploadLog Management Update Component', () => { + let comp: InputDataUploadLogUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let inputDataUploadLogFormService: InputDataUploadLogFormService; + let inputDataUploadLogService: InputDataUploadLogService; + let inputDataUploadService: InputDataUploadService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadLogUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(InputDataUploadLogUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(InputDataUploadLogUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + inputDataUploadLogFormService = TestBed.inject(InputDataUploadLogFormService); + inputDataUploadLogService = TestBed.inject(InputDataUploadLogService); + inputDataUploadService = TestBed.inject(InputDataUploadService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call InputDataUpload query and add missing value', () => { + const inputDataUploadLog: IInputDataUploadLog = { id: 456 }; + const inputDataUpload: IInputDataUpload = { id: 32591 }; + inputDataUploadLog.inputDataUpload = inputDataUpload; + + const inputDataUploadCollection: IInputDataUpload[] = [{ id: 13045 }]; + jest.spyOn(inputDataUploadService, 'query').mockReturnValue(of(new HttpResponse({ body: inputDataUploadCollection }))); + const additionalInputDataUploads = [inputDataUpload]; + const expectedCollection: IInputDataUpload[] = [...additionalInputDataUploads, ...inputDataUploadCollection]; + jest.spyOn(inputDataUploadService, 'addInputDataUploadToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputDataUploadLog }); + comp.ngOnInit(); + + expect(inputDataUploadService.query).toHaveBeenCalled(); + expect(inputDataUploadService.addInputDataUploadToCollectionIfMissing).toHaveBeenCalledWith( + inputDataUploadCollection, + ...additionalInputDataUploads.map(expect.objectContaining), + ); + expect(comp.inputDataUploadsSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const inputDataUploadLog: IInputDataUploadLog = { id: 456 }; + const inputDataUpload: IInputDataUpload = { id: 14978 }; + inputDataUploadLog.inputDataUpload = inputDataUpload; + + activatedRoute.data = of({ inputDataUploadLog }); + comp.ngOnInit(); + + expect(comp.inputDataUploadsSharedCollection).toContain(inputDataUpload); + expect(comp.inputDataUploadLog).toEqual(inputDataUploadLog); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputDataUploadLog = { id: 123 }; + jest.spyOn(inputDataUploadLogFormService, 'getInputDataUploadLog').mockReturnValue(inputDataUploadLog); + jest.spyOn(inputDataUploadLogService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputDataUploadLog }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: inputDataUploadLog })); + saveSubject.complete(); + + // THEN + expect(inputDataUploadLogFormService.getInputDataUploadLog).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(inputDataUploadLogService.update).toHaveBeenCalledWith(expect.objectContaining(inputDataUploadLog)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputDataUploadLog = { id: 123 }; + jest.spyOn(inputDataUploadLogFormService, 'getInputDataUploadLog').mockReturnValue({ id: null }); + jest.spyOn(inputDataUploadLogService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputDataUploadLog: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: inputDataUploadLog })); + saveSubject.complete(); + + // THEN + expect(inputDataUploadLogFormService.getInputDataUploadLog).toHaveBeenCalled(); + expect(inputDataUploadLogService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputDataUploadLog = { id: 123 }; + jest.spyOn(inputDataUploadLogService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputDataUploadLog }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(inputDataUploadLogService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareInputDataUpload', () => { + it('Should forward to inputDataUploadService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(inputDataUploadService, 'compareInputDataUpload'); + comp.compareInputDataUpload(entity, entity2); + expect(inputDataUploadService.compareInputDataUpload).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.ts b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.ts new file mode 100644 index 0000000..c26a9ae --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/update/input-data-upload-log-update.component.ts @@ -0,0 +1,107 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IInputDataUpload } from 'app/entities/input-data-upload/input-data-upload.model'; +import { InputDataUploadService } from 'app/entities/input-data-upload/service/input-data-upload.service'; +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; +import { InputDataUploadLogFormService, InputDataUploadLogFormGroup } from './input-data-upload-log-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-upload-log-update', + templateUrl: './input-data-upload-log-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class InputDataUploadLogUpdateComponent implements OnInit { + isSaving = false; + inputDataUploadLog: IInputDataUploadLog | null = null; + + inputDataUploadsSharedCollection: IInputDataUpload[] = []; + + protected inputDataUploadLogService = inject(InputDataUploadLogService); + protected inputDataUploadLogFormService = inject(InputDataUploadLogFormService); + protected inputDataUploadService = inject(InputDataUploadService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: InputDataUploadLogFormGroup = this.inputDataUploadLogFormService.createInputDataUploadLogFormGroup(); + + compareInputDataUpload = (o1: IInputDataUpload | null, o2: IInputDataUpload | null): boolean => + this.inputDataUploadService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ inputDataUploadLog }) => { + this.inputDataUploadLog = inputDataUploadLog; + if (inputDataUploadLog) { + this.updateForm(inputDataUploadLog); + } + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const inputDataUploadLog = this.inputDataUploadLogFormService.getInputDataUploadLog(this.editForm); + if (inputDataUploadLog.id !== null) { + this.subscribeToSaveResponse(this.inputDataUploadLogService.update(inputDataUploadLog)); + } else { + this.subscribeToSaveResponse(this.inputDataUploadLogService.create(inputDataUploadLog)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(inputDataUploadLog: IInputDataUploadLog): void { + this.inputDataUploadLog = inputDataUploadLog; + this.inputDataUploadLogFormService.resetForm(this.editForm, inputDataUploadLog); + + this.inputDataUploadsSharedCollection = this.inputDataUploadService.addEntityToCollectionIfMissing( + this.inputDataUploadsSharedCollection, + inputDataUploadLog.inputDataUpload, + ); + } + + protected loadRelationshipsOptions(): void { + this.inputDataUploadService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((inputDataUploads: IInputDataUpload[]) => + this.inputDataUploadService.addEntityToCollectionIfMissing( + inputDataUploads, + this.inputDataUploadLog?.inputDataUpload, + ), + ), + ) + .subscribe((inputDataUploads: IInputDataUpload[]) => (this.inputDataUploadsSharedCollection = inputDataUploads)); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.html b/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.html new file mode 100644 index 0000000..6b2af9c --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (inputDataUpload) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.spec.ts new file mode 100644 index 0000000..53f107e --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { InputDataUploadService } from '../service/input-data-upload.service'; + +import { InputDataUploadDeleteDialogComponent } from './input-data-upload-delete-dialog.component'; + +describe('InputDataUpload Management Delete Component', () => { + let comp: InputDataUploadDeleteDialogComponent; + let fixture: ComponentFixture; + let service: InputDataUploadService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(InputDataUploadDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(InputDataUploadDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataUploadService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.ts b/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.ts new file mode 100644 index 0000000..d667771 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/delete/input-data-upload-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IInputDataUpload } from '../input-data-upload.model'; +import { InputDataUploadService } from '../service/input-data-upload.service'; + +@Component({ + standalone: true, + templateUrl: './input-data-upload-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class InputDataUploadDeleteDialogComponent { + inputDataUpload?: IInputDataUpload; + + protected inputDataUploadService = inject(InputDataUploadService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.inputDataUploadService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.html b/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.html new file mode 100644 index 0000000..98ea82d --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.html @@ -0,0 +1,116 @@ + +
+
+ @if (inputDataUpload()) { +
+

+ Input Data Upload +

+ +
+ + + + + + + +
+
Period
+
+ @if (inputDataUpload()!.period) { + + } +
+
Owner
+
+ @if (inputDataUpload()!.owner) { + + } +
+
+ Title +
+
+ {{ inputDataUpload()!.title }} +
+
+ Type +
+
+ {{ + { null: '', INVENTORY: 'INVENTORY', ENERGY: 'ENERGY', GAS: 'GAS', GLOBAL: 'GLOBAL', ACCOUNTING: 'ACCOUNTING' }[inputDataUpload()!.type ?? 'null'] + }} +
+
+ Data File +
+
+ @if (inputDataUpload()!.dataFile) { +
+ Abrir + {{ inputDataUpload()!.dataFileContentType }}, {{ byteSize(inputDataUpload()!.dataFile ?? '') }} +
+ } +
+
+ State +
+
+ {{ + { null: '', UPLOADED: 'UPLOADED', PROCESSING: 'PROCESSING', PROCESSED: 'PROCESSED', ERROR: 'ERROR' }[ + inputDataUpload()!.state ?? 'null' + ] + }} +
+
+ Comments +
+
+ + + + + + + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.spec.ts b/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.spec.ts new file mode 100644 index 0000000..8c86b2d --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.spec.ts @@ -0,0 +1,93 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { DataUtils } from 'app/core/util/data-util.service'; + +import { InputDataUploadDetailComponent } from './input-data-upload-detail.component'; + +describe('InputDataUpload Management Detail Component', () => { + let comp: InputDataUploadDetailComponent; + let fixture: ComponentFixture; + let dataUtils: DataUtils; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InputDataUploadDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: InputDataUploadDetailComponent, + resolve: { inputDataUpload: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(InputDataUploadDetailComponent, '') + .compileComponents(); + dataUtils = TestBed.inject(DataUtils); + jest.spyOn(window, 'open').mockImplementation(() => null); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InputDataUploadDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load inputDataUpload on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', InputDataUploadDetailComponent); + + // THEN + expect(instance.inputDataUpload()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); + + describe('byteSize', () => { + it('Should call byteSize from DataUtils', () => { + // GIVEN + jest.spyOn(dataUtils, 'byteSize'); + const fakeBase64 = 'fake base64'; + + // WHEN + comp.byteSize(fakeBase64); + + // THEN + expect(dataUtils.byteSize).toHaveBeenCalledWith(fakeBase64); + }); + }); + + describe('openFile', () => { + it('Should call openFile from DataUtils', () => { + const newWindow = { ...window }; + newWindow.document.write = jest.fn(); + window.open = jest.fn(() => newWindow); + window.onload = jest.fn(() => newWindow) as any; + window.URL.createObjectURL = jest.fn() as any; + // GIVEN + jest.spyOn(dataUtils, 'openFile'); + const fakeContentType = 'fake content type'; + const fakeBase64 = 'fake base64'; + + // WHEN + comp.openFile(fakeBase64, fakeContentType); + + // THEN + expect(dataUtils.openFile).toHaveBeenCalledWith(fakeBase64, fakeContentType); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.ts b/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.ts new file mode 100644 index 0000000..d8245ef --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/detail/input-data-upload-detail.component.ts @@ -0,0 +1,93 @@ +import { Component, inject, input, OnInit } from '@angular/core'; +import { RouterModule, Router, ActivatedRoute } from '@angular/router'; +import { Observable, finalize, filter, tap, mergeMap, EMPTY, of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { AlertService } from 'app/core/util/alert.service'; +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { IInputDataUpload, NewInputDataUpload } from '../input-data-upload.model'; +import { InputDataUploadService } from '../service/input-data-upload.service'; +import { InputDataUploadLogLogsDialogComponent } from 'app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component'; +import { IActivityProgress } from 'app/resilient/resilient-activity/resilient-activity.model'; +import { ResilientActivityService } from 'app/resilient/resilient-activity/service/resilient-activity.service'; +import { UploadStatus } from 'app/entities/enumerations/upload-status.model'; +import { ResilientActivityActionsButtonComponent } from 'app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component'; +import { ResilientActivityProgressComponent } from 'app/resilient/resilient-activity/progress/resilient-activity-progress.component'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-upload-detail', + templateUrl: './input-data-upload-detail.component.html', + imports: [ + SharedModule, + RouterModule, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ResilientActivityActionsButtonComponent, + ResilientActivityProgressComponent + ], +}) +export class InputDataUploadDetailComponent extends AbstractResilientEntityComponent implements OnInit { + inputDataUpload = input(null); + + protected dataUtils = inject(DataUtils); + protected inputDataUploadService = inject(InputDataUploadService); + protected modalService = inject(NgbModal); + protected alertService = inject(AlertService); + protected router = inject(Router); + protected route = inject(ActivatedRoute); + protected resilientActivityService = inject(ResilientActivityService); + + public UploadStatus = UploadStatus; + + constructor(service: InputDataUploadService) { super(service); } + + ngOnInit(): void { + // Checks for messages in navigation state and shows the alert + const navState = window.history.state; + if (navState && navState.message) { + this.alertService.addAlert({ + type: 'success', + message: navState.message, + }); + } + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + previousState(): void { + window.history.back(); + } + + handleActivityInvoked(message: string): void { + this.reloadPage(); + } + + showLogs(): void { + const modalRef = this.modalService.open(InputDataUploadLogLogsDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.inputDataUpload = this.inputDataUpload(); + } + + /* Refresh the current record. Probably something was invoked in the server and the record mus be updated */ + reloadPage(): void { + // Get the current entity ID from the route + const entityId = this.route.snapshot.params['id']; + + // Navigate to the same detail page with the same ID + const currentUrl: string = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate(['/input-data-upload', entityId, 'view']); + }); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.html b/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.html new file mode 100644 index 0000000..dcea223 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.html @@ -0,0 +1,29 @@ +@if (inputDataUpload) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.spec.ts b/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.spec.ts new file mode 100644 index 0000000..6f2923f --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { InputDataUploadService } from '../service/input-data-upload.service'; + +import { InputDataUploadImportDialogComponent } from './input-data-upload-import-dialog.component'; + +describe('InputDataUpload Management Import Component', () => { + let comp: InputDataUploadImportDialogComponent; + let fixture: ComponentFixture; + let service: InputDataUploadService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadImportDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(InputDataUploadImportDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(InputDataUploadImportDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataUploadService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmImport', () => { + it('Should call import service on confirmImport', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'import').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmImport(123); + tick(); + + // THEN + expect(service.import).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('imported'); + }), + )); + + it('Should not call import service on clear', () => { + // GIVEN + jest.spyOn(service, 'import'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.import).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.ts b/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.ts new file mode 100644 index 0000000..7df7c23 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/import/input-data-upload-import-dialog.component.ts @@ -0,0 +1,34 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IInputDataUpload } from '../input-data-upload.model'; +import { InputDataUploadService } from '../service/input-data-upload.service'; + +export const IMPORT_EVENT = 'IMPORT_EVENT'; + +@Component({ + standalone: true, + templateUrl: './input-data-upload-import-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class InputDataUploadImportDialogComponent { + inputDataUpload?: IInputDataUpload; + + protected inputDataUploadService = inject(InputDataUploadService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmImport(id: number): void { + if (this.inputDataUpload) { + this.inputDataUploadService.doLoad(this.inputDataUpload.id).subscribe(() => { + this.activeModal.close(IMPORT_EVENT); + }); + } + } +} diff --git a/src/main/webapp/app/entities/input-data-upload/input-data-upload.model.ts b/src/main/webapp/app/entities/input-data-upload/input-data-upload.model.ts new file mode 100644 index 0000000..ab76ce3 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/input-data-upload.model.ts @@ -0,0 +1,23 @@ +import dayjs from 'dayjs/esm'; +import { IPeriod } from 'app/entities/period/period.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { UploadType } from 'app/entities/enumerations/upload-type.model'; +import { UploadStatus } from 'app/entities/enumerations/upload-status.model'; +import { IActivityDomain } from 'app/entities/activity/activity.model'; + +export interface IInputDataUpload extends IActivityDomain { + title?: string | null; + dataFile?: string | null; + dataFileContentType?: string | null; + uploadFileName?: string | null; + diskFileName?: string | null; + type?: keyof typeof UploadType | null; + state?: keyof typeof UploadStatus | null; + comments?: string | null; + creationDate?: dayjs.Dayjs | null; + creationUsername?: string | null; + period?: Pick | null; + owner?: Pick | null; +} + +export type NewInputDataUpload = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/input-data-upload/input-data-upload.routes.ts b/src/main/webapp/app/entities/input-data-upload/input-data-upload.routes.ts new file mode 100644 index 0000000..28f5c94 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/input-data-upload.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { InputDataUploadComponent } from './list/input-data-upload.component'; +import { InputDataUploadDetailComponent } from './detail/input-data-upload-detail.component'; +import { InputDataUploadUpdateComponent } from './update/input-data-upload-update.component'; +import InputDataUploadResolve from './route/input-data-upload-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; +import { InputDataUploadService } from './service/input-data-upload.service'; + +const inputDataUploadRoute: Routes = [ + { + path: '', + component: InputDataUploadComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: InputDataUploadService, + resource: Resources.INPUT_DATA_UPLOAD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: ':id/view', + component: InputDataUploadDetailComponent, + resolve: { + inputDataUpload: InputDataUploadResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: InputDataUploadService, + resource: Resources.INPUT_DATA_UPLOAD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: 'new', + component: InputDataUploadUpdateComponent, + resolve: { + inputDataUpload: InputDataUploadResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: InputDataUploadService, + resource: Resources.INPUT_DATA_UPLOAD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: ':id/edit', + component: InputDataUploadUpdateComponent, + resolve: { + inputDataUpload: InputDataUploadResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: InputDataUploadService, + resource: Resources.INPUT_DATA_UPLOAD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, +]; + +export default inputDataUploadRoute; diff --git a/src/main/webapp/app/entities/input-data-upload/input-data-upload.test-samples.ts b/src/main/webapp/app/entities/input-data-upload/input-data-upload.test-samples.ts new file mode 100644 index 0000000..70d1191 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/input-data-upload.test-samples.ts @@ -0,0 +1,59 @@ +import dayjs from 'dayjs/esm'; + +import { IInputDataUpload, NewInputDataUpload } from './input-data-upload.model'; + +export const sampleWithRequiredData: IInputDataUpload = { + id: 2690, + title: 'perky likewise floodplain', + dataFile: '../fake-data/blob/hipster.png', + dataFileContentType: 'unknown', + type: 'INVENTORY', + state: 'UPLOADED', + creationDate: dayjs('2024-10-15T04:00'), + creationUsername: 'outside contend', +}; + +export const sampleWithPartialData: IInputDataUpload = { + id: 23120, + title: 'happening churn budget', + dataFile: '../fake-data/blob/hipster.png', + dataFileContentType: 'unknown', + uploadFileName: 'apropos', + diskFileName: 'daily breadfruit', + type: 'ACCOUNTING', + state: 'PROCESSED', + creationDate: dayjs('2024-10-14T16:01'), + creationUsername: 'colon armoire encumber', + version: 18232, +}; + +export const sampleWithFullData: IInputDataUpload = { + id: 9005, + title: 'chess', + dataFile: '../fake-data/blob/hipster.png', + dataFileContentType: 'unknown', + uploadFileName: 'controvert utilized', + diskFileName: 'when beautifully regarding', + type: 'ACCOUNTING', + state: 'PROCESSING', + comments: 'secret whenever ugh', + creationDate: dayjs('2024-10-15T00:48'), + creationUsername: 'whereas overreact', + version: 4458, +}; + +export const sampleWithNewData: NewInputDataUpload = { + title: 'annual', + dataFile: '../fake-data/blob/hipster.png', + dataFileContentType: 'unknown', + type: 'ACCOUNTING', + state: 'PROCESSING', + creationDate: dayjs('2024-10-14T17:09'), + creationUsername: 'welcome which', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.html b/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.html new file mode 100644 index 0000000..e9d84e8 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.html @@ -0,0 +1,125 @@ + +
+

+ Input Data Uploads + +
+ + + +
+

+ + + + + + @if (inputDataUploads?.length === 0) { +
+ Nenhum Input Data Uploads encontrado +
+ } + + @if (inputDataUploads && inputDataUploads.length > 0) { +
+ + + + + + + + + + + + @for (inputDataUpload of inputDataUploads; track trackId) { + + + + + + + + } + +
+
+ Title + + +
+
+
+ Type + + +
+
+
+ State + + +
+
{{ inputDataUpload.title }} + {{ { null: '', INVENTORY: 'INVENTORY',ENERGY: 'ENERGY', GAS: 'GAS', GLOBAL: 'GLOBAL', ACCOUNTING: 'ACCOUNTING' }[inputDataUpload.type ?? 'null'] }} + + {{ + { null: '', UPLOADED: 'UPLOADED', PROCESSING: 'PROCESSING', PROCESSED: 'PROCESSED', ERROR: 'ERROR' }[ + inputDataUpload.state ?? 'null' + ] + }} + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.spec.ts b/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.spec.ts new file mode 100644 index 0000000..2c89d9c --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../input-data-upload.test-samples'; +import { InputDataUploadService } from '../service/input-data-upload.service'; + +import { InputDataUploadComponent } from './input-data-upload.component'; +import SpyInstance = jest.SpyInstance; + +describe('InputDataUpload Management Component', () => { + let comp: InputDataUploadComponent; + let fixture: ComponentFixture; + let service: InputDataUploadService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(InputDataUploadComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(InputDataUploadComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataUploadService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.inputDataUploads?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to inputDataUploadService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getInputDataUploadIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getInputDataUploadIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.ts b/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.ts new file mode 100644 index 0000000..67887bd --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/list/input-data-upload.component.ts @@ -0,0 +1,140 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { DataUtils } from 'app/core/util/data-util.service'; + +import { IInputDataUpload, NewInputDataUpload } from '../input-data-upload.model'; +import { EntityArrayResponseType, InputDataUploadService } from '../service/input-data-upload.service'; +import { InputDataUploadDeleteDialogComponent } from '../delete/input-data-upload-delete-dialog.component'; +import { UploadStatus } from 'app/entities/enumerations/upload-status.model'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; +import { SecurityPermission } from 'app/security/security-permission.model'; +import { Resources } from 'app/security/resources.model'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-upload', + templateUrl: './input-data-upload.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class InputDataUploadComponent extends AbstractResilientEntityComponent implements OnInit { + subscription: Subscription | null = null; + inputDataUploads?: IInputDataUpload[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public UploadStatus = UploadStatus; + public router = inject(Router); + protected inputDataUploadService = inject(InputDataUploadService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected dataUtils = inject(DataUtils); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IInputDataUpload): number => this.inputDataUploadService.getEntityIdentifier(item); + + constructor(service: InputDataUploadService) { super(service); } + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.inputDataUploads || this.inputDataUploads.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + return this.dataUtils.openFile(base64String, contentType); + } + + delete(inputDataUpload: IInputDataUpload): void { + const modalRef = this.modalService.open(InputDataUploadDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.inputDataUpload = inputDataUpload; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.inputDataUploads = this.refineData(dataFromBody); + } + + protected refineData(data: IInputDataUpload[]): IInputDataUpload[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IInputDataUpload[] | null): IInputDataUpload[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.inputDataUploadService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload/route/input-data-upload-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/input-data-upload/route/input-data-upload-routing-resolve.service.spec.ts new file mode 100644 index 0000000..ab0cb85 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/route/input-data-upload-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IInputDataUpload } from '../input-data-upload.model'; +import { InputDataUploadService } from '../service/input-data-upload.service'; + +import inputDataUploadResolve from './input-data-upload-routing-resolve.service'; + +describe('InputDataUpload routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: InputDataUploadService; + let resultInputDataUpload: IInputDataUpload | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(InputDataUploadService); + resultInputDataUpload = undefined; + }); + + describe('resolve', () => { + it('should return IInputDataUpload returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataUploadResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputDataUpload = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultInputDataUpload).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataUploadResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputDataUpload = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultInputDataUpload).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataUploadResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputDataUpload = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultInputDataUpload).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/route/input-data-upload-routing-resolve.service.ts b/src/main/webapp/app/entities/input-data-upload/route/input-data-upload-routing-resolve.service.ts new file mode 100644 index 0000000..d417e0d --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/route/input-data-upload-routing-resolve.service.ts @@ -0,0 +1,44 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { IInputDataUpload } from '../input-data-upload.model'; +import { InputDataUploadService } from '../service/input-data-upload.service'; + +const inputDataUploadResolve = (route: ActivatedRouteSnapshot): Observable => { + const router = inject(Router); + const translate = inject(TranslateService); + const inputDataUploadService = inject(InputDataUploadService); + const id = route.params['id']; + if (id) { + return inject(InputDataUploadService) + .find(id) + .pipe( + mergeMap((inputDataUploadResponse: HttpResponse) => { + if (inputDataUploadResponse.body) { + const inputDataUpload = inputDataUploadResponse.body; + + // Check if is /edit route, and the state of the InputDataUpload + const path = route.routeConfig?.path; + if (path && path.endsWith('/edit') && !inputDataUploadService.canUpdate(inputDataUpload)) { + router.navigate(['/input-data-upload', id, 'view'], { + state: { message: translate.instant('resilientApp.inputDataUpload.home.editNotAllowedForState') } + }); + return EMPTY; + } else { + return of(inputDataUpload); + } + } else { + router.navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default inputDataUploadResolve; diff --git a/src/main/webapp/app/entities/input-data-upload/service/input-data-upload.service.spec.ts b/src/main/webapp/app/entities/input-data-upload/service/input-data-upload.service.spec.ts new file mode 100644 index 0000000..e7ed214 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/service/input-data-upload.service.spec.ts @@ -0,0 +1,205 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IInputDataUpload } from '../input-data-upload.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../input-data-upload.test-samples'; + +import { InputDataUploadService, RestInputDataUpload } from './input-data-upload.service'; + +const requireRestSample: RestInputDataUpload = { + ...sampleWithRequiredData, + creationDate: sampleWithRequiredData.creationDate?.toJSON(), +}; + +describe('InputDataUpload Service', () => { + let service: InputDataUploadService; + let httpMock: HttpTestingController; + let expectedResult: IInputDataUpload | IInputDataUpload[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(InputDataUploadService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a InputDataUpload', () => { + const inputDataUpload = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(inputDataUpload).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a InputDataUpload', () => { + const inputDataUpload = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(inputDataUpload).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a InputDataUpload', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of InputDataUpload', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a InputDataUpload', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addInputDataUploadToCollectionIfMissing', () => { + it('should add a InputDataUpload to an empty array', () => { + const inputDataUpload: IInputDataUpload = sampleWithRequiredData; + expectedResult = service.addInputDataUploadToCollectionIfMissing([], inputDataUpload); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(inputDataUpload); + }); + + it('should not add a InputDataUpload to an array that contains it', () => { + const inputDataUpload: IInputDataUpload = sampleWithRequiredData; + const inputDataUploadCollection: IInputDataUpload[] = [ + { + ...inputDataUpload, + }, + sampleWithPartialData, + ]; + expectedResult = service.addInputDataUploadToCollectionIfMissing(inputDataUploadCollection, inputDataUpload); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a InputDataUpload to an array that doesn't contain it", () => { + const inputDataUpload: IInputDataUpload = sampleWithRequiredData; + const inputDataUploadCollection: IInputDataUpload[] = [sampleWithPartialData]; + expectedResult = service.addInputDataUploadToCollectionIfMissing(inputDataUploadCollection, inputDataUpload); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(inputDataUpload); + }); + + it('should add only unique InputDataUpload to an array', () => { + const inputDataUploadArray: IInputDataUpload[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const inputDataUploadCollection: IInputDataUpload[] = [sampleWithRequiredData]; + expectedResult = service.addInputDataUploadToCollectionIfMissing(inputDataUploadCollection, ...inputDataUploadArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const inputDataUpload: IInputDataUpload = sampleWithRequiredData; + const inputDataUpload2: IInputDataUpload = sampleWithPartialData; + expectedResult = service.addInputDataUploadToCollectionIfMissing([], inputDataUpload, inputDataUpload2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(inputDataUpload); + expect(expectedResult).toContain(inputDataUpload2); + }); + + it('should accept null and undefined values', () => { + const inputDataUpload: IInputDataUpload = sampleWithRequiredData; + expectedResult = service.addInputDataUploadToCollectionIfMissing([], null, inputDataUpload, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(inputDataUpload); + }); + + it('should return initial array if no InputDataUpload is added', () => { + const inputDataUploadCollection: IInputDataUpload[] = [sampleWithRequiredData]; + expectedResult = service.addInputDataUploadToCollectionIfMissing(inputDataUploadCollection, undefined, null); + expect(expectedResult).toEqual(inputDataUploadCollection); + }); + }); + + describe('compareInputDataUpload', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareInputDataUpload(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareInputDataUpload(entity1, entity2); + const compareResult2 = service.compareInputDataUpload(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareInputDataUpload(entity1, entity2); + const compareResult2 = service.compareInputDataUpload(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareInputDataUpload(entity1, entity2); + const compareResult2 = service.compareInputDataUpload(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/service/input-data-upload.service.ts b/src/main/webapp/app/entities/input-data-upload/service/input-data-upload.service.ts new file mode 100644 index 0000000..f63b649 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/service/input-data-upload.service.ts @@ -0,0 +1,65 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum } from 'app/resilient/resilient-base/resilient-base.service'; +import { IInputDataUpload, NewInputDataUpload } from '../input-data-upload.model'; +import { UploadStatus } from 'app/entities/enumerations/upload-status.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { Resources } from 'app/security/resources.model'; +import { SecurityAction } from 'app/security/security-action.model'; +import { SecurityPermission } from 'app/security/security-permission.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class InputDataUploadService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private accountService = inject(AccountService); + + /* ResilientService - Overriden methods */ + /* ************************************ */ + override canUpdate(inputDataUpload: IInputDataUpload): boolean { + // Check InputDataUpload State + if (inputDataUpload && inputDataUpload.state !== UploadStatus.UPLOADED) { + return false; + } + + // Check if the Period is still OPEN to allow changes + // TODO + + // Check if user permission is SecurityPermission.HIERARCHY. + // In this case, must check if user has access to inputDataUpload.owner + const securityPermission: SecurityPermission = this.accountService.getPermission(Resources.INPUT_DATA_UPLOAD, SecurityAction.UPDATE); + if (SecurityPermission.HIERARCHY === securityPermission) { + if (this.accountService.getParentOrganization()?.id != inputDataUpload.owner?.id) { + return false; + } + } + + return true; + } + + + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/input-data-uploads'); + override getResourceUrl(): string { return this.resourceUrl; } + + private readonly dateProperties: Map = new Map([ + ["creationDate", DateTypeEnum.TIMESTAMP] + ]); + override getDateProperties(): Map { return this.dateProperties; } + + + doLoad(id: number): Observable> { + return this.http.get(`${this.resourceUrl}/${id}/doLoad`, { observe: 'response' }); + } + + +} diff --git a/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-form.service.spec.ts b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-form.service.spec.ts new file mode 100644 index 0000000..b27312d --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-form.service.spec.ts @@ -0,0 +1,110 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../input-data-upload.test-samples'; + +import { InputDataUploadFormService } from './input-data-upload-form.service'; + +describe('InputDataUpload Form Service', () => { + let service: InputDataUploadFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(InputDataUploadFormService); + }); + + describe('Service methods', () => { + describe('createInputDataUploadFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createInputDataUploadFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + title: expect.any(Object), + dataFile: expect.any(Object), + uploadFileName: expect.any(Object), + diskFileName: expect.any(Object), + type: expect.any(Object), + state: expect.any(Object), + comments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + owner: expect.any(Object), + }), + ); + }); + + it('passing IInputDataUpload should create a new form with FormGroup', () => { + const formGroup = service.createInputDataUploadFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + title: expect.any(Object), + dataFile: expect.any(Object), + uploadFileName: expect.any(Object), + diskFileName: expect.any(Object), + type: expect.any(Object), + state: expect.any(Object), + comments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + owner: expect.any(Object), + }), + ); + }); + }); + + describe('getInputDataUpload', () => { + it('should return NewInputDataUpload for default InputDataUpload initial value', () => { + const formGroup = service.createInputDataUploadFormGroup(sampleWithNewData); + + const inputDataUpload = service.getInputDataUpload(formGroup) as any; + + expect(inputDataUpload).toMatchObject(sampleWithNewData); + }); + + it('should return NewInputDataUpload for empty InputDataUpload initial value', () => { + const formGroup = service.createInputDataUploadFormGroup(); + + const inputDataUpload = service.getInputDataUpload(formGroup) as any; + + expect(inputDataUpload).toMatchObject({}); + }); + + it('should return IInputDataUpload', () => { + const formGroup = service.createInputDataUploadFormGroup(sampleWithRequiredData); + + const inputDataUpload = service.getInputDataUpload(formGroup) as any; + + expect(inputDataUpload).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IInputDataUpload should not enable id FormControl', () => { + const formGroup = service.createInputDataUploadFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewInputDataUpload should disable id FormControl', () => { + const formGroup = service.createInputDataUploadFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-form.service.ts b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-form.service.ts new file mode 100644 index 0000000..ad3e996 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-form.service.ts @@ -0,0 +1,134 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { IInputDataUpload, NewInputDataUpload } from '../input-data-upload.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IInputDataUpload for edit and NewInputDataUploadFormGroupInput for create. + */ +type InputDataUploadFormGroupInput = IInputDataUpload | PartialWithRequiredKeyOf; + +/** + * Type that converts some properties for forms. + */ +type FormValueOf = Omit & { + creationDate?: string | null; +}; + +type InputDataUploadFormRawValue = FormValueOf; + +type NewInputDataUploadFormRawValue = FormValueOf; + +type InputDataUploadFormDefaults = Pick; + +type InputDataUploadFormGroupContent = { + id: FormControl; + title: FormControl; + dataFile: FormControl; + dataFileContentType: FormControl; + uploadFileName: FormControl; + diskFileName: FormControl; + type: FormControl; + state: FormControl; + comments: FormControl; + creationDate: FormControl; + creationUsername: FormControl; + version: FormControl; + period: FormControl; + owner: FormControl; +}; + +export type InputDataUploadFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class InputDataUploadFormService { + createInputDataUploadFormGroup(inputDataUpload: InputDataUploadFormGroupInput = { id: null }): InputDataUploadFormGroup { + const inputDataUploadRawValue = this.convertInputDataUploadToInputDataUploadRawValue({ + ...this.getFormDefaults(), + ...inputDataUpload, + }); + return new FormGroup({ + id: new FormControl( + { value: inputDataUploadRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + title: new FormControl(inputDataUploadRawValue.title, { + validators: [Validators.required], + }), + dataFile: new FormControl(inputDataUploadRawValue.dataFile, { + validators: [Validators.required], + }), + dataFileContentType: new FormControl(inputDataUploadRawValue.dataFileContentType), + uploadFileName: new FormControl(inputDataUploadRawValue.uploadFileName), + diskFileName: new FormControl(inputDataUploadRawValue.diskFileName), + type: new FormControl(inputDataUploadRawValue.type, { + validators: [Validators.required], + }), + state: new FormControl(inputDataUploadRawValue.state), + comments: new FormControl(inputDataUploadRawValue.comments), + creationDate: new FormControl(inputDataUploadRawValue.creationDate), + creationUsername: new FormControl(inputDataUploadRawValue.creationUsername), + version: new FormControl(inputDataUploadRawValue.version), + period: new FormControl(inputDataUploadRawValue.period, { + validators: [Validators.required], + }), + owner: new FormControl(inputDataUploadRawValue.owner, { + validators: [Validators.required], + }), + }); + } + + getInputDataUpload(form: InputDataUploadFormGroup): IInputDataUpload | NewInputDataUpload { + return this.convertInputDataUploadRawValueToInputDataUpload( + form.getRawValue() as InputDataUploadFormRawValue | NewInputDataUploadFormRawValue, + ); + } + + resetForm(form: InputDataUploadFormGroup, inputDataUpload: InputDataUploadFormGroupInput): void { + const inputDataUploadRawValue = this.convertInputDataUploadToInputDataUploadRawValue({ ...this.getFormDefaults(), ...inputDataUpload }); + form.reset( + { + ...inputDataUploadRawValue, + id: { value: inputDataUploadRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): InputDataUploadFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + creationDate: currentTime, + }; + } + + private convertInputDataUploadRawValueToInputDataUpload( + rawInputDataUpload: InputDataUploadFormRawValue | NewInputDataUploadFormRawValue, + ): IInputDataUpload | NewInputDataUpload { + return { + ...rawInputDataUpload, + creationDate: dayjs(rawInputDataUpload.creationDate, DATE_TIME_FORMAT), + }; + } + + private convertInputDataUploadToInputDataUploadRawValue( + inputDataUpload: IInputDataUpload | (Partial & InputDataUploadFormDefaults), + ): InputDataUploadFormRawValue | PartialWithRequiredKeyOf { + return { + ...inputDataUpload, + creationDate: inputDataUpload.creationDate ? inputDataUpload.creationDate.format(DATE_TIME_FORMAT) : undefined, + }; + } +} diff --git a/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.html b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.html new file mode 100644 index 0000000..c4a7fa2 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.html @@ -0,0 +1,195 @@ + +
+
+
+

+ Criar ou editar Input Data Upload +

+ +
+ +
+ + @if (editForm.controls.id.value == null && ownerOrganizationDefault == null) { + + } @else if (ownerOrganizationDefault != null) { + + } +
+
+ + @if (periodsSharedCollection.length==1) { + + } @else { + + } +
+
+ + + @if (editForm.get('title')!.invalid && (editForm.get('title')!.dirty || editForm.get('title')!.touched)) { +
+ @if (editForm.get('title')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + @if (uploadTypeValues.length==1) { + + } @else { + + @if (editForm.get('type')!.invalid && (editForm.get('type')!.dirty || editForm.get('type')!.touched)) { +
+ @if (editForm.get('type')?.errors?.required) { + O campo é obrigatório. + } +
+ } + } +
+
+ +
+ @if (editForm.get('dataFile')!.value) { +
+ Abrir
+ {{ editForm.get('dataFileContentType')!.value }}, {{ byteSize(editForm.get('dataFile')!.value!) }} + +
+ } + +
+ + + @if (editForm.get('dataFile')!.invalid && (editForm.get('dataFile')!.dirty || editForm.get('dataFile')!.touched)) { +
+ @if (editForm.get('dataFile')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + +
+ + @if (editForm.controls.id.value !== null) { +
+
+
+ State +
+
+ {{ inputDataUpload!.state }} +
+
+
+ } + +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.spec.ts b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.spec.ts new file mode 100644 index 0000000..cdcfae6 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.spec.ts @@ -0,0 +1,242 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { PeriodVersionService } from 'app/entities/period-version/service/period-version.service'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { IInputDataUpload } from '../input-data-upload.model'; +import { InputDataUploadService } from '../service/input-data-upload.service'; +import { InputDataUploadFormService } from './input-data-upload-form.service'; + +import { InputDataUploadUpdateComponent } from './input-data-upload-update.component'; + +describe('InputDataUpload Management Update Component', () => { + let comp: InputDataUploadUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let inputDataUploadFormService: InputDataUploadFormService; + let inputDataUploadService: InputDataUploadService; + let periodService: PeriodService; + let periodVersionService: PeriodVersionService; + let organizationService: OrganizationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(InputDataUploadUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(InputDataUploadUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + inputDataUploadFormService = TestBed.inject(InputDataUploadFormService); + inputDataUploadService = TestBed.inject(InputDataUploadService); + periodService = TestBed.inject(PeriodService); + periodVersionService = TestBed.inject(PeriodVersionService); + organizationService = TestBed.inject(OrganizationService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call Period query and add missing value', () => { + const inputDataUpload: IInputDataUpload = { id: 456 }; + const period: IPeriod = { id: 11829 }; + inputDataUpload.period = period; + + const periodCollection: IPeriod[] = [{ id: 7640 }]; + jest.spyOn(periodService, 'query').mockReturnValue(of(new HttpResponse({ body: periodCollection }))); + const additionalPeriods = [period]; + const expectedCollection: IPeriod[] = [...additionalPeriods, ...periodCollection]; + jest.spyOn(periodService, 'addPeriodToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputDataUpload }); + comp.ngOnInit(); + + expect(periodService.query).toHaveBeenCalled(); + expect(periodService.addPeriodToCollectionIfMissing).toHaveBeenCalledWith( + periodCollection, + ...additionalPeriods.map(expect.objectContaining), + ); + expect(comp.periodsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call PeriodVersion query and add missing value', () => { + const inputDataUpload: IInputDataUpload = { id: 456 }; + const periodVersion: IPeriodVersion = { id: 26730 }; + inputDataUpload.periodVersion = periodVersion; + + const periodVersionCollection: IPeriodVersion[] = [{ id: 18123 }]; + jest.spyOn(periodVersionService, 'query').mockReturnValue(of(new HttpResponse({ body: periodVersionCollection }))); + const additionalPeriodVersions = [periodVersion]; + const expectedCollection: IPeriodVersion[] = [...additionalPeriodVersions, ...periodVersionCollection]; + jest.spyOn(periodVersionService, 'addPeriodVersionToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputDataUpload }); + comp.ngOnInit(); + + expect(periodVersionService.query).toHaveBeenCalled(); + expect(periodVersionService.addPeriodVersionToCollectionIfMissing).toHaveBeenCalledWith( + periodVersionCollection, + ...additionalPeriodVersions.map(expect.objectContaining), + ); + expect(comp.periodVersionsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call Organization query and add missing value', () => { + const inputDataUpload: IInputDataUpload = { id: 456 }; + const owner: IOrganization = { id: 24978 }; + inputDataUpload.owner = owner; + + const organizationCollection: IOrganization[] = [{ id: 32111 }]; + jest.spyOn(organizationService, 'query').mockReturnValue(of(new HttpResponse({ body: organizationCollection }))); + const additionalOrganizations = [owner]; + const expectedCollection: IOrganization[] = [...additionalOrganizations, ...organizationCollection]; + jest.spyOn(organizationService, 'addOrganizationToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputDataUpload }); + comp.ngOnInit(); + + expect(organizationService.query).toHaveBeenCalled(); + expect(organizationService.addOrganizationToCollectionIfMissing).toHaveBeenCalledWith( + organizationCollection, + ...additionalOrganizations.map(expect.objectContaining), + ); + expect(comp.organizationsSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const inputDataUpload: IInputDataUpload = { id: 456 }; + const period: IPeriod = { id: 3902 }; + inputDataUpload.period = period; + const periodVersion: IPeriodVersion = { id: 7134 }; + inputDataUpload.periodVersion = periodVersion; + const owner: IOrganization = { id: 5813 }; + inputDataUpload.owner = owner; + + activatedRoute.data = of({ inputDataUpload }); + comp.ngOnInit(); + + expect(comp.periodsSharedCollection).toContain(period); + expect(comp.periodVersionsSharedCollection).toContain(periodVersion); + expect(comp.organizationsSharedCollection).toContain(owner); + expect(comp.inputDataUpload).toEqual(inputDataUpload); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputDataUpload = { id: 123 }; + jest.spyOn(inputDataUploadFormService, 'getInputDataUpload').mockReturnValue(inputDataUpload); + jest.spyOn(inputDataUploadService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputDataUpload }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: inputDataUpload })); + saveSubject.complete(); + + // THEN + expect(inputDataUploadFormService.getInputDataUpload).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(inputDataUploadService.update).toHaveBeenCalledWith(expect.objectContaining(inputDataUpload)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputDataUpload = { id: 123 }; + jest.spyOn(inputDataUploadFormService, 'getInputDataUpload').mockReturnValue({ id: null }); + jest.spyOn(inputDataUploadService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputDataUpload: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: inputDataUpload })); + saveSubject.complete(); + + // THEN + expect(inputDataUploadFormService.getInputDataUpload).toHaveBeenCalled(); + expect(inputDataUploadService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputDataUpload = { id: 123 }; + jest.spyOn(inputDataUploadService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputDataUpload }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(inputDataUploadService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('comparePeriod', () => { + it('Should forward to periodService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(periodService, 'comparePeriod'); + comp.comparePeriod(entity, entity2); + expect(periodService.comparePeriod).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('comparePeriodVersion', () => { + it('Should forward to periodVersionService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(periodVersionService, 'comparePeriodVersion'); + comp.comparePeriodVersion(entity, entity2); + expect(periodVersionService.comparePeriodVersion).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareOrganization', () => { + it('Should forward to organizationService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(organizationService, 'compareOrganization'); + comp.compareOrganization(entity, entity2); + expect(organizationService.compareOrganization).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.ts b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.ts new file mode 100644 index 0000000..820a1a9 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload/update/input-data-upload-update.component.ts @@ -0,0 +1,269 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, first, Subscription, of } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { AlertError } from 'app/shared/alert/alert-error.model'; +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; +import { DataUtils, FileLoadError } from 'app/core/util/data-util.service'; +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { UploadType } from 'app/entities/enumerations/upload-type.model'; +import { UploadStatus } from 'app/entities/enumerations/upload-status.model'; +import { InputDataUploadService } from '../service/input-data-upload.service'; +import { IInputDataUpload } from '../input-data-upload.model'; +import { InputDataUploadFormService, InputDataUploadFormGroup } from './input-data-upload-form.service'; +import { ResilientEnvironmentService } from 'app/resilient/resilient-environment/service/resilient-environment.service'; +import { AccountService } from 'app/core/auth/account.service'; +import { Resources } from 'app/security/resources.model'; +import { SecurityAction } from 'app/security/security-action.model'; +import { SecurityPermission } from 'app/security/security-permission.model'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; +import { Authority } from 'app/config/authority.constants'; +import { PeriodStatus } from 'app/entities/enumerations/period-status.model'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-upload-update', + templateUrl: './input-data-upload-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class InputDataUploadUpdateComponent implements OnInit { + // Properties with defaults by Environment + ownerOrganizationDefault : IOrganization | null = null; + subscriptionWorkOrganization!: Subscription; // Store the subscription + + isSaving = false; + isLoading = false; + inputDataUpload: IInputDataUpload | null = null; + uploadStatusValues = Object.keys(UploadStatus); + + periodsSharedCollection: IPeriod[] = []; + organizationsSharedCollection: IOrganization[] = []; + uploadTypeValues: string[] = []; + + protected dataUtils = inject(DataUtils); + protected eventManager = inject(EventManager); + protected inputDataUploadService = inject(InputDataUploadService); + protected inputDataUploadFormService = inject(InputDataUploadFormService); + protected periodService = inject(PeriodService); + protected organizationService = inject(OrganizationService); + protected activatedRoute = inject(ActivatedRoute); + protected accountService = inject(AccountService); + protected envService = inject(ResilientEnvironmentService); + protected router = inject(Router); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: InputDataUploadFormGroup = this.inputDataUploadFormService.createInputDataUploadFormGroup(); + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => this.periodService.compareEntity(o1, o2); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + ngOnInit(): void { + this.isLoading=true; + + this.editForm.get('period')?.valueChanges + .subscribe(value => { + this.periodValueChange(value); + }); + + this.activatedRoute.data.subscribe(({ inputDataUpload }) => { + this.inputDataUpload = inputDataUpload; + if (inputDataUpload) { + // Check state of inputDataUpload. Edit is only allowed in UPLOADED state + if (inputDataUpload.state !== UploadStatus.UPLOADED) { + //Auto redirect to VIEW mode. With a friendly message explaining the redirect + this.router.navigate(['/input-data-upload', inputDataUpload.id, 'view'], { + state: { message: 'O estado do registo não permite a edição.' } + }); + } + + // Editing. Set defaults form HTML form behavior + this.ownerOrganizationDefault = inputDataUpload.owner; + + this.updateForm(inputDataUpload); + this.loadRelationshipsOptions(); // Load all relations + + this.isLoading=false; + } else { + // Create a new InputDataUpload + // Get the currently selected ORGANIZATION, by ResilientEnvironmentService + this.subscriptionWorkOrganization = this + .envService + .selectedWorkOrganization + /* .pipe(first()) // READS the Observable only once. Doesn't react to later changes on the subscription */ + .subscribe(org => { + if (org === undefined) { + //Do nothing. + } else { + // If I already have a owner default, ignore user changes + if (this.ownerOrganizationDefault==null) { + this.ownerOrganizationDefault = org; + if(org){ + // Load the Owner Organization list (only this one) + this.loadRelationshipsOptionsOrganization(org); + } else { + this.loadRelationshipsOptionsOrganization(null); + } + + this.loadRelationshipsOptionsPeriod(); + } + } + }); + } + + }); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + setFileData(event: Event, field: string, isImage: boolean): void { + this.dataUtils.loadFileToForm(event, this.editForm, field, isImage).subscribe({ + error: (err: FileLoadError) => + this.eventManager.broadcast(new EventWithContent('resilientApp.error', { ...err, key: 'error.file.' + err.key })), + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const inputDataUpload = this.inputDataUploadFormService.getInputDataUpload(this.editForm); + if (inputDataUpload.id !== null) { + this.subscribeToSaveResponse(this.inputDataUploadService.update(inputDataUpload)); + } else { + this.subscribeToSaveResponse(this.inputDataUploadService.create(inputDataUpload)); + } + } + + periodValueChange(selectedPeriod: any): void { + + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(inputDataUpload: IInputDataUpload): void { + this.inputDataUpload = inputDataUpload; + this.inputDataUploadFormService.resetForm(this.editForm, inputDataUpload); + + this.periodsSharedCollection = this.periodService.addEntityToCollectionIfMissing( + this.periodsSharedCollection, + inputDataUpload.period, + ); + this.organizationsSharedCollection = this.organizationService.addEntityToCollectionIfMissing( + this.organizationsSharedCollection, + inputDataUpload.owner, + ); + } + + protected loadRelationshipsOptions(): void { + this.loadRelationshipsOptionsPeriod(); + this.loadRelationshipsOptionsOrganization(null); + } + + protected loadRelationshipsOptionsPeriod(): void { + // Only active Period's + this.periodService + .queryByState(PeriodStatus.OPENED) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe(map((periods: IPeriod[]) => this.periodService.addEntityToCollectionIfMissing(periods, this.inputDataUpload?.period))) + .subscribe((periods: IPeriod[]) => { + this.periodsSharedCollection = periods; + + // If there's only one, auto-select + if (this.periodsSharedCollection.length === 1) { + this.editForm.patchValue({ period: this.periodsSharedCollection[0] }); + } + this.isLoading=false; + }); + } + + protected loadRelationshipsOptionsOrganization(organization: IOrganization | null): void { + this.setUploadTypeValuesCollection(); + + this.queryOrganization() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((organizations: IOrganization[]) => + this.organizationService.addEntityToCollectionIfMissing(organizations, this.inputDataUpload?.owner), + ), + ) + .subscribe((organizations: IOrganization[]) => { + this.organizationsSharedCollection = organizations; + + if (organization) { + this.ownerOrganizationDefault = this.organizationsSharedCollection.find(org => org.id === organization.id) ?? null; + this.editForm.patchValue({ owner: this.ownerOrganizationDefault }); + } + }); + } + + private setUploadTypeValuesCollection(): void { + // Distinguish the organization collection, if user has access to ALL or only HIERARCHY + if (this.accountService.hasAnyAuthority( [Authority.ADMIN, Authority.MANAGER])) { + this.uploadTypeValues = Object.keys(UploadType); + } else if (this.accountService.hasAnyAuthority(Authority.COORDINATOR)) { + this.uploadTypeValues = [UploadType.INVENTORY]; + } else { + this.uploadTypeValues = []; + } + + if (this.uploadTypeValues.length == 1) { + this.editForm.patchValue({ type: this.uploadTypeValues[0] as UploadType}); + } + } + + private queryOrganization(): Observable> { + // Distinguish the organization collection, if user has access to ALL or only HIERARCHY + let permission: SecurityPermission = SecurityPermission.NONE; + if (this.inputDataUpload?.id) { + permission = this.accountService.getPermission(Resources.INPUT_DATA_UPLOAD, SecurityAction.UPDATE); + } else { + permission = this.accountService.getPermission(Resources.INPUT_DATA_UPLOAD, SecurityAction.CREATE); + } + + if (permission === SecurityPermission.ALL) { + // Access to all + return this.organizationService.queryByNature(OrganizationNature.ORGANIZATION); + } if (permission === SecurityPermission.HIERARCHY) { + // Only user hierarchy + return this.organizationService.queryByNatureAndHierarchy(OrganizationNature.ORGANIZATION); + } else { + // NOTHING + return of(new HttpResponse({ body: [], status: 200 })); + } + } +} diff --git a/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.html b/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.html new file mode 100644 index 0000000..cc971b8 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (inputData) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.spec.ts new file mode 100644 index 0000000..a7e318c --- /dev/null +++ b/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { InputDataService } from '../service/input-data.service'; + +import { InputDataDeleteDialogComponent } from './input-data-delete-dialog.component'; + +describe('InputData Management Delete Component', () => { + let comp: InputDataDeleteDialogComponent; + let fixture: ComponentFixture; + let service: InputDataService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(InputDataDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(InputDataDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.ts b/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.ts new file mode 100644 index 0000000..23774e6 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/delete/input-data-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IInputData } from '../input-data.model'; +import { InputDataService } from '../service/input-data.service'; + +@Component({ + standalone: true, + templateUrl: './input-data-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class InputDataDeleteDialogComponent { + inputData?: IInputData; + + protected inputDataService = inject(InputDataService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.inputDataService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.html b/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.html new file mode 100644 index 0000000..1e0f986 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.html @@ -0,0 +1,221 @@ +
+
+ @if (inputData()) { +
+

Input Data

+ +
+ + + + + +
+
Código
+
+ {{ inputData()!.id }} +
+ +
Period
+
+ @if (inputData()!.period) { + + } +
+
Owner
+
+ @if (inputData()!.owner) { + + } +
+ +
Variable
+
+ @if (inputData()!.variable) { + + } +
+ +
Variable Class
+
+ @if (inputData()!.variableClassName) { +
+ {{ inputData()!.variableClassName }} +
+ } +
+
+ Source Value +
+
+ {{ inputData()!.sourceValue }} +
+
Source Unit
+
+ @if (inputData()!.sourceUnit) { + + } +
+ +
+ Variable Value +
+
+ {{ inputData()!.variableValue }} +
+
Unit
+
+ @if (inputData()!.unit) { + + } +
+ +
+ Imputed Value +
+
+ {{ inputData()!.imputedValue }} +
+
+ Source Type +
+
+ {{ + { null: '', MANUAL: 'MANUAL', FILE: 'FILE', AUTO: 'AUTO', DISTRIB: 'DISTRIB' }[inputData()!.sourceType ?? 'null'] + }} +
+
+ Data Date +
+
+ {{ inputData()!.dataDate | formatMediumDate }} +
+
+ Change Date +
+
+ {{ inputData()!.changeDate | formatMediumDatetime }} +
+
+ Change Username +
+
+ {{ inputData()!.changeUsername }} +
+
+ Data Source +
+
+ {{ inputData()!.dataSource }} +
+
+ Data User +
+
+ {{ inputData()!.dataUser }} +
+
+ Data Comments +
+
+ {{ inputData()!.dataComments }} +
+
+ Creation Date +
+
+ {{ inputData()!.creationDate | formatMediumDatetime }} +
+
+ Creation Username +
+
+ {{ inputData()!.creationUsername }} +
+
+ Version +
+
+ {{ inputData()!.version }} +
+ + +
Source Input Data
+
+ @if (inputData()!.sourceInputData) { + + } +
+
Source Input Data Upload
+
+ @if (inputData()!.sourceInputDataUpload) { + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.spec.ts b/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.spec.ts new file mode 100644 index 0000000..6927e59 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { InputDataDetailComponent } from './input-data-detail.component'; + +describe('InputData Management Detail Component', () => { + let comp: InputDataDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InputDataDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: InputDataDetailComponent, + resolve: { inputData: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(InputDataDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InputDataDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load inputData on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', InputDataDetailComponent); + + // THEN + expect(instance.inputData()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.ts b/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.ts new file mode 100644 index 0000000..88e5ded --- /dev/null +++ b/src/main/webapp/app/entities/input-data/detail/input-data-detail.component.ts @@ -0,0 +1,24 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IInputData, NewInputData } from '../input-data.model'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; +import { InputDataService } from '../service/input-data.service'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-detail', + templateUrl: './input-data-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class InputDataDetailComponent extends AbstractResilientEntityComponent { + inputData = input(null); + + constructor(service: InputDataService) { super(service); } + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/input-data/input-data.model.ts b/src/main/webapp/app/entities/input-data/input-data.model.ts new file mode 100644 index 0000000..fd6b0dd --- /dev/null +++ b/src/main/webapp/app/entities/input-data/input-data.model.ts @@ -0,0 +1,54 @@ +import dayjs from 'dayjs/esm'; +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariable } from 'app/entities/variable/variable.model'; +import { IPeriod } from 'app/entities/period/period.model'; +import { IUnit } from 'app/entities/unit/unit.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { IInputDataUpload } from 'app/entities/input-data-upload/input-data-upload.model'; +import { DataSourceType } from 'app/entities/enumerations/data-source-type.model'; + +export interface IInputData extends IResilientEntity { + sourceValue?: number | null; + variableValue?: number | null; + imputedValue?: number | null; + stringValue?: string | null; + sourceType?: keyof typeof DataSourceType | null; + dataDate?: dayjs.Dayjs | null; + changeDate?: dayjs.Dayjs | null; + changeUsername?: string | null; + dataSource?: string | null; + dataUser?: string | null; + dataComments?: string | null; + creationDate?: dayjs.Dayjs | null; + creationUsername?: string | null; + variableClassCode?: string | null; + variableClassName?: string | null; + variable?: Pick | null; + period?: Pick | null; + sourceUnit?: Pick | null; + unit?: Pick | null; + owner?: Pick | null; + sourceInputData?: Pick | null; + sourceInputDataUpload?: Pick | null; +} + +export type NewInputData = Omit & { id: null }; + +export type SearchInputData = Omit< + IInputData, + // Remove unwanted properties for search + 'id' | 'variable' | 'period' | 'periodVersion' | 'sourceUnit' | 'owner' | 'sourceInputData' | 'sourceInputDataUpload' | 'creationDate' | 'changeDate'> + & + // Add specific properties for search + { + id?: number | null; + variableId?: number | null; + variableNameOrCode?: string | null; + periodId?: number | null; + ownerId?: number | null; + sourceInputDataUploadId?: number | null; + variableScopeId?: number | null; + variableCategoryId?: number | null; + variableClassNameOrCode?: string | null; + }; diff --git a/src/main/webapp/app/entities/input-data/input-data.routes.ts b/src/main/webapp/app/entities/input-data/input-data.routes.ts new file mode 100644 index 0000000..2957bc4 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/input-data.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { InputDataComponent } from './list/input-data.component'; +import { InputDataDetailComponent } from './detail/input-data-detail.component'; +import { InputDataUpdateComponent } from './update/input-data-update.component'; +import InputDataResolve from './route/input-data-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; +import { InputDataService } from './service/input-data.service'; + +const inputDataRoute: Routes = [ + { + path: '', + component: InputDataComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: InputDataService, + resource: Resources.INPUT_DATA, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: ':id/view', + component: InputDataDetailComponent, + resolve: { + inputData: InputDataResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: InputDataService, + resource: Resources.INPUT_DATA, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: 'new', + component: InputDataUpdateComponent, + resolve: { + inputData: InputDataResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: InputDataService, + resource: Resources.INPUT_DATA, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: ':id/edit', + component: InputDataUpdateComponent, + resolve: { + inputData: InputDataResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: InputDataService, + resource: Resources.INPUT_DATA, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, +]; + +export default inputDataRoute; diff --git a/src/main/webapp/app/entities/input-data/input-data.test-samples.ts b/src/main/webapp/app/entities/input-data/input-data.test-samples.ts new file mode 100644 index 0000000..41b842f --- /dev/null +++ b/src/main/webapp/app/entities/input-data/input-data.test-samples.ts @@ -0,0 +1,63 @@ +import dayjs from 'dayjs/esm'; + +import { IInputData, NewInputData } from './input-data.model'; + +export const sampleWithRequiredData: IInputData = { + id: 16944, + sourceValue: 21379.99, + variableValue: 29223.59, + imputedValue: 10636.15, + sourceType: 'DISTRIB', + changeDate: dayjs('2024-10-15T05:39'), + changeUsername: 'fluid including tackle', + creationDate: dayjs('2024-10-14T17:28'), + creationUsername: 'fully', +}; + +export const sampleWithPartialData: IInputData = { + id: 23147, + sourceValue: 13759.42, + variableValue: 3099.36, + imputedValue: 8504.79, + sourceType: 'FILE', + changeDate: dayjs('2024-10-14T22:12'), + changeUsername: 'fourths cruelly', + dataUser: 'sleepily default cannibal', + creationDate: dayjs('2024-10-15T09:15'), + creationUsername: 'competence counselling', + version: 31911, +}; + +export const sampleWithFullData: IInputData = { + id: 938, + sourceValue: 13503.88, + variableValue: 17193.53, + imputedValue: 9372.84, + sourceType: 'MANUAL', + dataDate: dayjs('2024-10-15'), + changeDate: dayjs('2024-10-15T02:36'), + changeUsername: 'justly ouch', + dataSource: 'shrivel unlike medical', + dataUser: 'prioritise frozen', + dataComments: 'finally', + creationDate: dayjs('2024-10-14T19:32'), + creationUsername: 'briskly consequently', + version: 21303, +}; + +export const sampleWithNewData: NewInputData = { + sourceValue: 31581.72, + variableValue: 31193.23, + imputedValue: 7344.8, + sourceType: 'FILE', + changeDate: dayjs('2024-10-15T03:23'), + changeUsername: 'boohoo pirate capitalise', + creationDate: dayjs('2024-10-15T03:04'), + creationUsername: 'so emergence', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/input-data/list/input-data-search-form.service.spec.ts b/src/main/webapp/app/entities/input-data/list/input-data-search-form.service.spec.ts new file mode 100644 index 0000000..98cd416 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/list/input-data-search-form.service.spec.ts @@ -0,0 +1,126 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../input-data.test-samples'; + +import { InputDataSearchFormService } from './input-data-search-form.service'; + +describe('InputData Search Form Service', () => { + let service: InputDataSearchFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(InputDataSearchFormService); + }); + + describe('Service methods', () => { + describe('createInputDatasearchFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createInputDataSearchFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + + it('passing SearchInputData should create a new form with FormGroup', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + }); + + describe('getInputData', () => { + it('should return SearchInputData for default InputData initial value', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithNewData); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject(sampleWithNewData); + }); + + it('should return SearchInputData for empty InputData initial value', () => { + const formGroup = service.createInputDataSearchFormGroup(); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject({}); + }); + + it('should return SearchInputData', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithRequiredData); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IInputData should not enable id FormControl', () => { + const formGroup = service.createInputDataSearchFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing SearchInputData should disable id FormControl', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/list/input-data-search-form.service.ts b/src/main/webapp/app/entities/input-data/list/input-data-search-form.service.ts new file mode 100644 index 0000000..a238a2f --- /dev/null +++ b/src/main/webapp/app/entities/input-data/list/input-data-search-form.service.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { SearchInputData } from '../input-data.model'; +import { SearchFormService } from 'app/entities/shared/shared-search-form.service'; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts SearchInputData, for search. in the list + */ +type InputDataSearchFormGroupInput = SearchInputData; + +type InputDataSearchFormDefaults = SearchInputData; + +type InputDataSearchFormGroupContent = { + id: FormControl; + sourceValue: FormControl; + variableValue: FormControl; + imputedValue: FormControl; + sourceType: FormControl; + dataDate: FormControl; + changeUsername: FormControl; + dataSource: FormControl; + dataUser: FormControl; + dataComments: FormControl; + creationUsername: FormControl; + version: FormControl; + variableId: FormControl; + variableNameOrCode: FormControl; + variableClassNameOrCode: FormControl; + periodId: FormControl; + ownerId: FormControl; + sourceInputDataUploadId: FormControl; + variableScopeId: FormControl; + variableCategoryId: FormControl; +}; + +export type InputDataSearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class InputDataSearchFormService extends SearchFormService { + createInputDataSearchFormGroup(inputData: InputDataSearchFormGroupInput = { id: null }): InputDataSearchFormGroup { + const inputDataRawValue = { + ...this.getSearchFormDefaults(), + ...inputData, + }; + return new FormGroup({ + id: new FormControl(inputDataRawValue.id), + sourceValue: new FormControl(inputDataRawValue.sourceValue), + variableValue: new FormControl(inputDataRawValue.variableValue), + imputedValue: new FormControl(inputDataRawValue.imputedValue), + sourceType: new FormControl(inputDataRawValue.sourceType), + dataDate: new FormControl(inputDataRawValue.dataDate), + changeUsername: new FormControl(inputDataRawValue.changeUsername), + dataSource: new FormControl(inputDataRawValue.dataSource), + dataUser: new FormControl(inputDataRawValue.dataUser), + dataComments: new FormControl(inputDataRawValue.dataComments), + creationUsername: new FormControl(inputDataRawValue.creationUsername), + version: new FormControl(inputDataRawValue.version), + variableId: new FormControl(inputDataRawValue.variableId), + variableNameOrCode: new FormControl(inputDataRawValue.variableNameOrCode), + variableClassNameOrCode: new FormControl(inputDataRawValue.variableClassNameOrCode), + periodId: new FormControl(inputDataRawValue.periodId), + ownerId: new FormControl(inputDataRawValue.ownerId), + sourceInputDataUploadId: new FormControl(inputDataRawValue.sourceInputDataUploadId), + variableScopeId: new FormControl(inputDataRawValue.variableScopeId), + variableCategoryId: new FormControl(inputDataRawValue.variableCategoryId), + }); + } + + getInputData(form: InputDataSearchFormGroup): SearchInputData { + return form.getRawValue() as SearchInputData; + } + + resetForm(form: InputDataSearchFormGroup, inputData: InputDataSearchFormGroupInput): void { + const inputDataRawValue = { ...this.getSearchFormDefaults(), ...inputData }; + form.reset( + { + ...inputDataRawValue, + id: { value: inputDataRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): InputDataSearchFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/input-data/list/input-data.component.css b/src/main/webapp/app/entities/input-data/list/input-data.component.css new file mode 100644 index 0000000..6fd04cc --- /dev/null +++ b/src/main/webapp/app/entities/input-data/list/input-data.component.css @@ -0,0 +1,37 @@ +.form-group { + display: flex; + flex-direction: column; +} + +.search-form label { + font-weight: bold; + margin-bottom: 5px; +} + +.search-form select, .search-form button { + padding: 8px; + font-size: 16px; + width: 100%; /* Makes sure they fill the column */ +} + +.search-form button { + cursor: pointer; + background-color: #007BFF; + color: white; + border: none; + padding: 10px; + font-weight: bold; +} + +.search-form button:hover { + background-color: #0056b3; +} + +.search-form .btn-refresh { + min-width: 200px; + margin: 0px; +} + +#searchForm .form-group { + width: 100%; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/input-data/list/input-data.component.html b/src/main/webapp/app/entities/input-data/list/input-data.component.html new file mode 100644 index 0000000..4a0b28e --- /dev/null +++ b/src/main/webapp/app/entities/input-data/list/input-data.component.html @@ -0,0 +1,262 @@ +
+

+ Input Data + +
+ + + + +
+

+ + + + + + {{ ' ' // Search form }} +
+
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + + @if (inputData) { +
+ {{ ' ' // Wrap the all table in a form. This is the only way reactiveforms will work }} +
+ + + + + + + + + + + + + {{ ' ' // Search row }} + + + + + + + + + + @for (inputData of inputData; track trackId) { + + + + + + + + + + } + +
+
+ Variable + +
+
+
+ Class + +
+
+
+ Value + +
+
+
+ Unit + +
+
+ @if (inputData.variable) { + + } + + @if (inputData.variable) { +
+ {{ inputData.variable.name }} +
+ } +
+ @if (inputData.variableClassName) { +
+ {{ inputData.variableClassName }} +
+ } +
+ @if (inputData.imputedValue) { +
+ {{ inputData.imputedValue }} +
+ } +
+ @if (inputData.unit) { +
+ {{ inputData.unit.symbol }} +
+ } +
+
+ + + Visualizar + + + + + Editar + + + +
+
+ +
+
+ } + + @if (inputData?.length === 0) { +
+ Nenhum Input Data encontrado para os critérios e para a {{selectedOrganization?.name}} +
+ } +
diff --git a/src/main/webapp/app/entities/input-data/list/input-data.component.spec.ts b/src/main/webapp/app/entities/input-data/list/input-data.component.spec.ts new file mode 100644 index 0000000..a22c2dd --- /dev/null +++ b/src/main/webapp/app/entities/input-data/list/input-data.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../input-data.test-samples'; +import { InputDataService } from '../service/input-data.service'; + +import { InputDataComponent } from './input-data.component'; +import SpyInstance = jest.SpyInstance; + +describe('InputData Management Component', () => { + let comp: InputDataComponent; + let fixture: ComponentFixture; + let service: InputDataService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(InputDataComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(InputDataComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.inputData?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to inputDataService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getInputDataIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getInputDataIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/list/input-data.component.ts b/src/main/webapp/app/entities/input-data/list/input-data.component.ts new file mode 100644 index 0000000..2054f0a --- /dev/null +++ b/src/main/webapp/app/entities/input-data/list/input-data.component.ts @@ -0,0 +1,270 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, EMPTY, Observable, Subscription, tap, map } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IInputData, NewInputData } from '../input-data.model'; +import { IPeriod } from 'app/entities/period/period.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { EntityArrayResponseType, InputDataService } from '../service/input-data.service'; +import { InputDataDeleteDialogComponent } from '../delete/input-data-delete-dialog.component'; +import { InputDataSearchFormService, InputDataSearchFormGroup } from './input-data-search-form.service'; +import { ResilientEnvironmentService } from 'app/resilient/resilient-environment/service/resilient-environment.service'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +@Component({ + standalone: true, + selector: 'jhi-input-data', + templateUrl: './input-data.component.html', + styleUrl: './input-data.component.css', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + MatProgressSpinnerModule, + ], +}) +export class InputDataComponent extends AbstractResilientEntityComponent implements OnInit { + subscriptionWorkOrganization!: Subscription; // Store the subscription + selectedOrganization?: IOrganization; + subscription: Subscription | null = null; + inputData?: IInputData[]; + isLoading = false; + isExporting: boolean = false; + + sortState = sortStateSignal({}); + + scopesSharedCollection: IVariableScope[] = []; + categoriesSharedCollection: IVariableCategory[] = []; + periodsSharedCollection: IPeriod[] = []; + orgsSharedCollection: IOrganization[] = []; + + public router = inject(Router); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + protected periodService = inject(PeriodService); + protected organizationService = inject(OrganizationService); + protected inputDataService = inject(InputDataService); + protected inputDataSearchFormService = inject(InputDataSearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + protected envService = inject(ResilientEnvironmentService); + + searchForm: InputDataSearchFormGroup = this.inputDataSearchFormService.createInputDataSearchFormGroup(); + trackId = (_index: number, item: IInputData): number => this.inputDataService.getEntityIdentifier(item); + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => + this.periodService.compareEntity(o1, o2); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + compareScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + compareCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + constructor(service: InputDataService) { super(service); } + + ngOnInit(): void { + // Get the currently selected ORGANIZATION, by ResilientEnvironmentService + this.subscriptionWorkOrganization = this + .envService + .selectedWorkOrganization + /* .pipe(first()) // READS the Observable only once. Doesn't react to later changes on the subscription */ + .subscribe(org => { + if(org){ + this.selectedOrganization = org; + + // Set filter + this.searchForm.get('ownerId')?.setValue(org.id); + + // Clear list + this.inputData = undefined; + + // Execute search + if (this.searchForm.valid) { + this.load(); + } + } + }); + + // Listen to changes to the scope filter property + this.searchForm.get('variableScopeId')?.valueChanges.subscribe((scopeId) => { + this.loadCategoriesOfSelectedScope(scopeId); + this.searchForm.get('variableCategoryId')?.setValue(null); + }); + + this.loadRelationshipsOptions().subscribe(() => { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.inputData || this.inputData.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + }); + } + + delete(inputData: IInputData): void { + const modalRef = this.modalService.open(InputDataDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.inputData = inputData; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + onSearch(): void { + this.load(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + /* this.inputData = this.refineData(dataFromBody); */ + this.inputData = dataFromBody; + } + + protected refineData(data: IInputData[]): IInputData[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IInputData[] | null): IInputData[] { + return data ?? []; + } + + protected queryBackend(): Observable { + + this.isLoading = true; + const queryObject: any = { + eagerload: true, + }; + + const searchModel = this.inputDataSearchFormService.getInputData(this.searchForm); + if (searchModel) { + // Apply conditions + this.inputDataSearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + } + + return this.inputDataService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } + + onPeriodChange(event: Event) { + const selectedValue = (event.target as HTMLSelectElement).value; + this.searchForm.get('periodId')?.setValue(Number(selectedValue)); + } + + protected loadRelationshipsOptions(): Observable { + return new Observable((observer) => { + this.periodService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((periods: IPeriod[]) => { + this.periodsSharedCollection = periods; + + // Set first Period as default. It's mandatory to select a Period + this.searchForm.get('periodId')?.setValue(this.periodsSharedCollection[0]?.id); + + observer.next(); // Notify that process is complete + observer.complete(); + }); + + this.variableScopeService + .queryForData() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((scopes: IVariableScope[]) => (this.scopesSharedCollection = scopes)); + + + this.organizationService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((orgs: IOrganization[]) => (this.orgsSharedCollection = orgs)); + + }); + } + + protected loadCategoriesOfSelectedScope(scopeId: number|null|undefined): void { + if (scopeId) { + this.variableCategoryService + .queryByScopeForData(scopeId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((cats: IVariableCategory[]) => (this.categoriesSharedCollection = cats)); + } else { + this.categoriesSharedCollection = []; + } + } + + export(): void { + this.isExporting = true; + const periodId = this.searchForm.get('periodId')?.value; + + this.inputDataService.export(periodId!).subscribe((blob: Blob) => { + const a = document.createElement('a'); + const objectUrl = URL.createObjectURL(blob); + a.href = objectUrl; + a.download = 'report.xlsx'; + a.click(); + URL.revokeObjectURL(objectUrl); + + this.isExporting = false; + }); + } +} diff --git a/src/main/webapp/app/entities/input-data/route/input-data-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/input-data/route/input-data-routing-resolve.service.spec.ts new file mode 100644 index 0000000..7f4fc7c --- /dev/null +++ b/src/main/webapp/app/entities/input-data/route/input-data-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IInputData } from '../input-data.model'; +import { InputDataService } from '../service/input-data.service'; + +import inputDataResolve from './input-data-routing-resolve.service'; + +describe('InputData routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: InputDataService; + let resultInputData: IInputData | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(InputDataService); + resultInputData = undefined; + }); + + describe('resolve', () => { + it('should return IInputData returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputData = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultInputData).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputData = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultInputData).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + inputDataResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultInputData = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultInputData).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/route/input-data-routing-resolve.service.ts b/src/main/webapp/app/entities/input-data/route/input-data-routing-resolve.service.ts new file mode 100644 index 0000000..fc87688 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/route/input-data-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IInputData } from '../input-data.model'; +import { InputDataService } from '../service/input-data.service'; + +const inputDataResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(InputDataService) + .find(id) + .pipe( + mergeMap((inputData: HttpResponse) => { + if (inputData.body) { + return of(inputData.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default inputDataResolve; diff --git a/src/main/webapp/app/entities/input-data/service/input-data.service.spec.ts b/src/main/webapp/app/entities/input-data/service/input-data.service.spec.ts new file mode 100644 index 0000000..6d3d6b6 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/service/input-data.service.spec.ts @@ -0,0 +1,208 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { DATE_FORMAT } from 'app/config/input.constants'; +import { IInputData } from '../input-data.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../input-data.test-samples'; + +import { InputDataService, RestInputData } from './input-data.service'; + +const requireRestSample: RestInputData = { + ...sampleWithRequiredData, + dataDate: sampleWithRequiredData.dataDate?.format(DATE_FORMAT), + changeDate: sampleWithRequiredData.changeDate?.toJSON(), + creationDate: sampleWithRequiredData.creationDate?.toJSON(), +}; + +describe('InputData Service', () => { + let service: InputDataService; + let httpMock: HttpTestingController; + let expectedResult: IInputData | IInputData[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(InputDataService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a InputData', () => { + const inputData = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(inputData).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a InputData', () => { + const inputData = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(inputData).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a InputData', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of InputData', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a InputData', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addInputDataToCollectionIfMissing', () => { + it('should add a InputData to an empty array', () => { + const inputData: IInputData = sampleWithRequiredData; + expectedResult = service.addInputDataToCollectionIfMissing([], inputData); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(inputData); + }); + + it('should not add a InputData to an array that contains it', () => { + const inputData: IInputData = sampleWithRequiredData; + const inputDataCollection: IInputData[] = [ + { + ...inputData, + }, + sampleWithPartialData, + ]; + expectedResult = service.addInputDataToCollectionIfMissing(inputDataCollection, inputData); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a InputData to an array that doesn't contain it", () => { + const inputData: IInputData = sampleWithRequiredData; + const inputDataCollection: IInputData[] = [sampleWithPartialData]; + expectedResult = service.addInputDataToCollectionIfMissing(inputDataCollection, inputData); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(inputData); + }); + + it('should add only unique InputData to an array', () => { + const inputDataArray: IInputData[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const inputDataCollection: IInputData[] = [sampleWithRequiredData]; + expectedResult = service.addInputDataToCollectionIfMissing(inputDataCollection, ...inputDataArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const inputData: IInputData = sampleWithRequiredData; + const inputData2: IInputData = sampleWithPartialData; + expectedResult = service.addInputDataToCollectionIfMissing([], inputData, inputData2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(inputData); + expect(expectedResult).toContain(inputData2); + }); + + it('should accept null and undefined values', () => { + const inputData: IInputData = sampleWithRequiredData; + expectedResult = service.addInputDataToCollectionIfMissing([], null, inputData, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(inputData); + }); + + it('should return initial array if no InputData is added', () => { + const inputDataCollection: IInputData[] = [sampleWithRequiredData]; + expectedResult = service.addInputDataToCollectionIfMissing(inputDataCollection, undefined, null); + expect(expectedResult).toEqual(inputDataCollection); + }); + }); + + describe('compareInputData', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareInputData(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareInputData(entity1, entity2); + const compareResult2 = service.compareInputData(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareInputData(entity1, entity2); + const compareResult2 = service.compareInputData(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareInputData(entity1, entity2); + const compareResult2 = service.compareInputData(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/service/input-data.service.ts b/src/main/webapp/app/entities/input-data/service/input-data.service.ts new file mode 100644 index 0000000..d277e74 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/service/input-data.service.ts @@ -0,0 +1,77 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; + +import dayjs from 'dayjs/esm'; + +import { isPresent } from 'app/core/util/operators'; +import { DATE_FORMAT } from 'app/config/input.constants'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IInputData, NewInputData } from '../input-data.model'; +import { SecurityPermission } from 'app/security/security-permission.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class InputDataService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private accountService = inject(AccountService); + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/input-data'); + override getResourceUrl(): string { return this.resourceUrl; } + + private readonly dateProperties: Map = new Map([ + ["dataDate", DateTypeEnum.DATE], + ["changeDate", DateTypeEnum.TIMESTAMP], + ["creationDate", DateTypeEnum.TIMESTAMP] + ]); + override getDateProperties(): Map { return this.dateProperties; } + + export(periodId: number): Observable { + return this.http.get(`${this.resourceUrl}/export/${periodId}`, { responseType: 'blob' }); + } + + /* ResilientService - Security Overriden methods */ + /* ********************************************* */ + override canUpdate(inputData: IInputData): boolean { + // Check if the Period is still OPEN to allow changes + // TODO + + // Check if user permission is SecurityPermission.HIERARCHY. + // In this case, must check if user has access to inputDataUpload.owner + const securityPermission: SecurityPermission = this.accountService.getPermission(Resources.INPUT_DATA, SecurityAction.UPDATE); + if (SecurityPermission.HIERARCHY === securityPermission) { + if (this.accountService.getParentOrganization()?.id != inputData.owner?.id) { + return false; + } + } + + return true; + } + + override canDelete(inputData: IInputData): boolean { + // Check if the Period is still OPEN to allow changes. Delete will remove data from the Period + // TODO + + // Check if user permission is SecurityPermission.HIERARCHY. + // In this case, must check if user has access to inputDataUpload.owner + const securityPermission: SecurityPermission = this.accountService.getPermission(Resources.INPUT_DATA, SecurityAction.DELETE); + if (SecurityPermission.HIERARCHY === securityPermission) { + if (this.accountService.getParentOrganization()?.id != inputData.owner?.id) { + return false; + } + } + + return true; + } +} diff --git a/src/main/webapp/app/entities/input-data/update/input-data-form.service.spec.ts b/src/main/webapp/app/entities/input-data/update/input-data-form.service.spec.ts new file mode 100644 index 0000000..303f0fd --- /dev/null +++ b/src/main/webapp/app/entities/input-data/update/input-data-form.service.spec.ts @@ -0,0 +1,126 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../input-data.test-samples'; + +import { InputDataFormService } from './input-data-form.service'; + +describe('InputData Form Service', () => { + let service: InputDataFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(InputDataFormService); + }); + + describe('Service methods', () => { + describe('createInputDataFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createInputDataFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + + it('passing IInputData should create a new form with FormGroup', () => { + const formGroup = service.createInputDataFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + }); + + describe('getInputData', () => { + it('should return NewInputData for default InputData initial value', () => { + const formGroup = service.createInputDataFormGroup(sampleWithNewData); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject(sampleWithNewData); + }); + + it('should return NewInputData for empty InputData initial value', () => { + const formGroup = service.createInputDataFormGroup(); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject({}); + }); + + it('should return IInputData', () => { + const formGroup = service.createInputDataFormGroup(sampleWithRequiredData); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IInputData should not enable id FormControl', () => { + const formGroup = service.createInputDataFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewInputData should disable id FormControl', () => { + const formGroup = service.createInputDataFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/update/input-data-form.service.ts b/src/main/webapp/app/entities/input-data/update/input-data-form.service.ts new file mode 100644 index 0000000..7663363 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/update/input-data-form.service.ts @@ -0,0 +1,168 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { IInputData, NewInputData } from '../input-data.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IInputData for edit and NewInputDataFormGroupInput for create. + */ +type InputDataFormGroupInput = IInputData | PartialWithRequiredKeyOf; + +/** + * Type that converts some properties for forms. + */ +type FormValueOf = Omit & { + changeDate?: string | null; + creationDate?: string | null; +}; + +type InputDataFormRawValue = FormValueOf; + +type NewInputDataFormRawValue = FormValueOf; + +type InputDataFormDefaults = Pick; + +type InputDataFormGroupContent = { + id: FormControl; + sourceValue: FormControl; + variableValue: FormControl; + imputedValue: FormControl; + stringValue: FormControl; + sourceType: FormControl; + dataDate: FormControl; + changeDate: FormControl; + changeUsername: FormControl; + dataSource: FormControl; + dataUser: FormControl; + dataComments: FormControl; + creationDate: FormControl; + creationUsername: FormControl; + variableClassCode: FormControl; + variableClassName: FormControl; + version: FormControl; + variable: FormControl; + period: FormControl; + sourceUnit: FormControl; + unit: FormControl; + owner: FormControl; + sourceInputData: FormControl; + sourceInputDataUpload: FormControl; +}; + +export type InputDataFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class InputDataFormService { + createInputDataFormGroup(inputData: InputDataFormGroupInput = { id: null }): InputDataFormGroup { + const inputDataRawValue = this.convertInputDataToInputDataRawValue({ + ...this.getFormDefaults(), + ...inputData, + }); + return new FormGroup({ + id: new FormControl( + { value: inputDataRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + period: new FormControl(inputDataRawValue.period, { + validators: [Validators.required], + }), + owner: new FormControl(inputDataRawValue.owner, { + validators: [Validators.required], + }), + variable: new FormControl(inputDataRawValue.variable, { + validators: [Validators.required], + }), + sourceUnit: new FormControl(inputDataRawValue.sourceUnit, { + validators: [Validators.required], + }), + unit: new FormControl(inputDataRawValue.unit), + sourceValue: new FormControl(inputDataRawValue.sourceValue, { + validators: [Validators.required], + }), + variableValue: new FormControl(inputDataRawValue.variableValue, { + validators: [Validators.required], + }), + imputedValue: new FormControl(inputDataRawValue.imputedValue, { + validators: [Validators.required], + }), + sourceType: new FormControl(inputDataRawValue.sourceType, { + validators: [Validators.required], + }), + stringValue: new FormControl(inputDataRawValue.stringValue), + dataDate: new FormControl(inputDataRawValue.dataDate), + changeDate: new FormControl(inputDataRawValue.changeDate, { + validators: [Validators.required], + }), + changeUsername: new FormControl(inputDataRawValue.changeUsername, { + validators: [Validators.required], + }), + dataSource: new FormControl(inputDataRawValue.dataSource), + dataUser: new FormControl(inputDataRawValue.dataUser), + dataComments: new FormControl(inputDataRawValue.dataComments), + creationDate: new FormControl(inputDataRawValue.creationDate, { + validators: [Validators.required], + }), + creationUsername: new FormControl(inputDataRawValue.creationUsername, { + validators: [Validators.required], + }), + variableClassCode: new FormControl(inputDataRawValue.variableClassCode), + variableClassName: new FormControl(inputDataRawValue.variableClassName), + version: new FormControl(inputDataRawValue.version), + sourceInputData: new FormControl(inputDataRawValue.sourceInputData), + sourceInputDataUpload: new FormControl(inputDataRawValue.sourceInputDataUpload), + }); + } + + getInputData(form: InputDataFormGroup): IInputData | NewInputData { + return this.convertInputDataRawValueToInputData(form.getRawValue() as InputDataFormRawValue | NewInputDataFormRawValue); + } + + resetForm(form: InputDataFormGroup, inputData: InputDataFormGroupInput): void { + const inputDataRawValue = this.convertInputDataToInputDataRawValue({ ...this.getFormDefaults(), ...inputData }); + form.reset( + { + ...inputDataRawValue, + id: { value: inputDataRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): InputDataFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + changeDate: currentTime, + creationDate: currentTime, + }; + } + + private convertInputDataRawValueToInputData(rawInputData: InputDataFormRawValue | NewInputDataFormRawValue): IInputData | NewInputData { + return { + ...rawInputData, + changeDate: dayjs(rawInputData.changeDate, DATE_TIME_FORMAT), + creationDate: dayjs(rawInputData.creationDate, DATE_TIME_FORMAT), + }; + } + + private convertInputDataToInputDataRawValue( + inputData: IInputData | (Partial & InputDataFormDefaults), + ): InputDataFormRawValue | PartialWithRequiredKeyOf { + return { + ...inputData, + changeDate: inputData.changeDate ? inputData.changeDate.format(DATE_TIME_FORMAT) : undefined, + creationDate: inputData.creationDate ? inputData.creationDate.format(DATE_TIME_FORMAT) : undefined, + }; + } +} diff --git a/src/main/webapp/app/entities/input-data/update/input-data-update.component.css b/src/main/webapp/app/entities/input-data/update/input-data-update.component.css new file mode 100644 index 0000000..2a4715a --- /dev/null +++ b/src/main/webapp/app/entities/input-data/update/input-data-update.component.css @@ -0,0 +1,38 @@ +.no-spinner::-webkit-inner-spin-button, +.no-spinner::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.no-spinner { + -moz-appearance: textfield; +} + +.spinner-container { + display: inline-flex; + align-items: center; + width: 100%; + justify-content: space-between; + gap: 4px; /* optional: adds spacing between value and spinner */ + } + + .select-with-spinner { + position: relative; + display: inline-block; + width: 100%; + } + + .select-with-spinner select { + width: 100%; + padding-right: 30px; /* leave room for spinner */ + } + + .spinner { + position: absolute; + top: 50%; + right: 30px; + transform: translateY(-50%); + width: 14px; + height: 14px; + pointer-events: none; /* let clicks go to the select */ + } \ No newline at end of file diff --git a/src/main/webapp/app/entities/input-data/update/input-data-update.component.html b/src/main/webapp/app/entities/input-data/update/input-data-update.component.html new file mode 100644 index 0000000..a6e55e9 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/update/input-data-update.component.html @@ -0,0 +1,470 @@ +
+
+
+

+ Criar ou editar Input Data +

+ +
+ + + + @if (false && editForm.controls.id.value !== null) { +
+
+
+ + +
+
+
+ } +
{{ '' // Period | Owner }} +
+ + @if (editForm.controls.id.value == null) { + + } @else { + {{ inputData?.period?.name }} + } +
+
+ + @if (editForm.controls.id.value == null) { + + } @else { + {{ inputData?.owner?.name }} + } +
+
+
{{ '' // Variable }} +
+
+ + @if (editForm.controls.id.value == null) { + {{'' // It's creating, make the variable editable}} +
+ + + + +
+ } @else { + {{'' // Can't change the variable}} + {{ inputData?.variable?.name }} + } +
+
+
+
{{ '' // Source Unit | Base Unit }} +
+ + +
+
+ + @if (editForm.controls.id.value == null) { + {{'' // It's creating, bind to editForm value}} +  {{ editForm.get('unit')?.value?.name }} + } @else { + {{'' // It's editing, bind to inputdata value}} +  {{ inputData?.unit?.name }} + } +
+
+
{{ '' // Source Value | Variable Value | Imputed Value }} +
+
+ + + @if (editForm.get('sourceValue')!.invalid && (editForm.get('sourceValue')!.dirty || editForm.get('sourceValue')!.touched)) { +
+ @if (editForm.get('sourceValue')?.errors?.required) { + O campo é obrigatório. + } + Este campo é do tipo número. +
+ } +
+
+
+
+ {{ '' // AUTOMATIC. Not user editable. Value calculated from sourceValue }} + + + + {{ editForm.get('variableValue')?.value }}  + + + @if (variableValueHasError) { +
+ Ocorreu um erro na conversão do valor para a unidade base. +
+ } +
+
+
+
+ {{ '' // AUTOMATIC. Not user editable. Value calculated from variableValue deducted of distribution }} + + + {{ editForm.get('imputedValue')?.value }}  +
+
+
+
{{ '' // String Value }} +
+
+ + + +
+
+
+
{{ '' // Variable Class }} +
+
+ +
+ + + + +
+
+
+
+
{{ '' // Source | Comments }} +
+
+ + +
+
+
+
+ + +
+
+
+
{{ '' // Source Type | Input Data Upload | Input Data }} +
+
+ + @if (editForm.controls.id.value == null) { + {{'' // It's creating, bind to editForm value}} + {{ 'resilientApp.DataSourceType.' + editForm.get('sourceType')?.value | translate }} + } @else { + {{'' // It's editing, bind to inputData value}} + {{ 'resilientApp.DataSourceType.' + inputData?.sourceType | translate }} + } +
+
+
+
+ + {{'' // Can't change the InputDataUpload reference }} + {{ inputData?.sourceInputDataUpload?.id }}  +
+
+
+
+ + {{'' // Can't change the InputData reference }} + {{ inputData?.sourceInputData?.id }}  +
+
+
+
{{ '' // Data Reference Date | Creation Date | Change Date }} +
+
+ +
+ + +
+
+
+
+
+ {{ '' // AUTOMATIC. Not user editable}} + + @if (editForm.controls.id.value == null) { + {{'' // It's creating, bind to editForm value}} + {{ editForm.get('creationDate')?.value }} + } @else { + {{'' // It's editing, bind to inputData value}} + {{ inputData?.creationDate }} + } +
+
+
+
+ {{ '' // AUTOMATIC. Not user editable}} + + {{ inputData?.changeDate }}  +
+
+
+
{{ '' // Aquired By | Created By | Changed By }} +
+
+ + +
+
+
+
+ {{ '' // AUTOMATIC. Not user editable}} + + {{ editForm.get('creationUsername')?.value }} +
+
+
+
+ {{ '' // AUTOMATIC. Not user editable}} + + {{ editForm.get('changeUsername')?.value }} +
+
+
+ + + +
+ + + +
+
+
+
+
diff --git a/src/main/webapp/app/entities/input-data/update/input-data-update.component.spec.ts b/src/main/webapp/app/entities/input-data/update/input-data-update.component.spec.ts new file mode 100644 index 0000000..b17a33c --- /dev/null +++ b/src/main/webapp/app/entities/input-data/update/input-data-update.component.spec.ts @@ -0,0 +1,399 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IVariable } from 'app/entities/variable/variable.model'; +import { VariableService } from 'app/entities/variable/service/variable.service'; +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { PeriodVersionService } from 'app/entities/period-version/service/period-version.service'; +import { IUnit } from 'app/entities/unit/unit.model'; +import { UnitService } from 'app/entities/unit/service/unit.service'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { IInputDataUpload } from 'app/entities/input-data-upload/input-data-upload.model'; +import { InputDataUploadService } from 'app/entities/input-data-upload/service/input-data-upload.service'; +import { IInputData } from '../input-data.model'; +import { InputDataService } from '../service/input-data.service'; +import { InputDataFormService } from './input-data-form.service'; + +import { InputDataUpdateComponent } from './input-data-update.component'; + +describe('InputData Management Update Component', () => { + let comp: InputDataUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let inputDataFormService: InputDataFormService; + let inputDataService: InputDataService; + let variableService: VariableService; + let periodService: PeriodService; + let periodVersionService: PeriodVersionService; + let unitService: UnitService; + let organizationService: OrganizationService; + let inputDataUploadService: InputDataUploadService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(InputDataUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(InputDataUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + inputDataFormService = TestBed.inject(InputDataFormService); + inputDataService = TestBed.inject(InputDataService); + variableService = TestBed.inject(VariableService); + periodService = TestBed.inject(PeriodService); + periodVersionService = TestBed.inject(PeriodVersionService); + unitService = TestBed.inject(UnitService); + organizationService = TestBed.inject(OrganizationService); + inputDataUploadService = TestBed.inject(InputDataUploadService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call InputData query and add missing value', () => { + const inputData: IInputData = { id: 456 }; + const sourceInputData: IInputData = { id: 27513 }; + inputData.sourceInputData = sourceInputData; + + const inputDataCollection: IInputData[] = [{ id: 3796 }]; + jest.spyOn(inputDataService, 'query').mockReturnValue(of(new HttpResponse({ body: inputDataCollection }))); + const additionalInputData = [sourceInputData]; + const expectedCollection: IInputData[] = [...additionalInputData, ...inputDataCollection]; + jest.spyOn(inputDataService, 'addInputDataToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(inputDataService.query).toHaveBeenCalled(); + expect(inputDataService.addInputDataToCollectionIfMissing).toHaveBeenCalledWith( + inputDataCollection, + ...additionalInputData.map(expect.objectContaining), + ); + expect(comp.inputDataSharedCollection).toEqual(expectedCollection); + }); + + it('Should call Variable query and add missing value', () => { + const inputData: IInputData = { id: 456 }; + const variable: IVariable = { id: 25598 }; + inputData.variable = variable; + + const variableCollection: IVariable[] = [{ id: 4654 }]; + jest.spyOn(variableService, 'query').mockReturnValue(of(new HttpResponse({ body: variableCollection }))); + const additionalVariables = [variable]; + const expectedCollection: IVariable[] = [...additionalVariables, ...variableCollection]; + jest.spyOn(variableService, 'addVariableToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(variableService.query).toHaveBeenCalled(); + expect(variableService.addVariableToCollectionIfMissing).toHaveBeenCalledWith( + variableCollection, + ...additionalVariables.map(expect.objectContaining), + ); + expect(comp.variablesSharedCollection).toEqual(expectedCollection); + }); + + it('Should call Period query and add missing value', () => { + const inputData: IInputData = { id: 456 }; + const period: IPeriod = { id: 30428 }; + inputData.period = period; + + const periodCollection: IPeriod[] = [{ id: 25867 }]; + jest.spyOn(periodService, 'query').mockReturnValue(of(new HttpResponse({ body: periodCollection }))); + const additionalPeriods = [period]; + const expectedCollection: IPeriod[] = [...additionalPeriods, ...periodCollection]; + jest.spyOn(periodService, 'addPeriodToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(periodService.query).toHaveBeenCalled(); + expect(periodService.addPeriodToCollectionIfMissing).toHaveBeenCalledWith( + periodCollection, + ...additionalPeriods.map(expect.objectContaining), + ); + expect(comp.periodsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call PeriodVersion query and add missing value', () => { + const inputData: IInputData = { id: 456 }; + const periodVersion: IPeriodVersion = { id: 28923 }; + inputData.periodVersion = periodVersion; + + const periodVersionCollection: IPeriodVersion[] = [{ id: 30543 }]; + jest.spyOn(periodVersionService, 'query').mockReturnValue(of(new HttpResponse({ body: periodVersionCollection }))); + const additionalPeriodVersions = [periodVersion]; + const expectedCollection: IPeriodVersion[] = [...additionalPeriodVersions, ...periodVersionCollection]; + jest.spyOn(periodVersionService, 'addPeriodVersionToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(periodVersionService.query).toHaveBeenCalled(); + expect(periodVersionService.addPeriodVersionToCollectionIfMissing).toHaveBeenCalledWith( + periodVersionCollection, + ...additionalPeriodVersions.map(expect.objectContaining), + ); + expect(comp.periodVersionsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call Unit query and add missing value', () => { + const inputData: IInputData = { id: 456 }; + const sourceUnit: IUnit = { id: 17465 }; + inputData.sourceUnit = sourceUnit; + const unit: IUnit = { id: 19595 }; + inputData.unit = unit; + + const unitCollection: IUnit[] = [{ id: 32254 }]; + jest.spyOn(unitService, 'query').mockReturnValue(of(new HttpResponse({ body: unitCollection }))); + const additionalUnits = [sourceUnit, unit]; + const expectedCollection: IUnit[] = [...additionalUnits, ...unitCollection]; + jest.spyOn(unitService, 'addUnitToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(unitService.query).toHaveBeenCalled(); + expect(unitService.addUnitToCollectionIfMissing).toHaveBeenCalledWith( + unitCollection, + ...additionalUnits.map(expect.objectContaining), + ); + expect(comp.unitsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call Organization query and add missing value', () => { + const inputData: IInputData = { id: 456 }; + const owner: IOrganization = { id: 28415 }; + inputData.owner = owner; + + const organizationCollection: IOrganization[] = [{ id: 31358 }]; + jest.spyOn(organizationService, 'query').mockReturnValue(of(new HttpResponse({ body: organizationCollection }))); + const additionalOrganizations = [owner]; + const expectedCollection: IOrganization[] = [...additionalOrganizations, ...organizationCollection]; + jest.spyOn(organizationService, 'addOrganizationToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(organizationService.query).toHaveBeenCalled(); + expect(organizationService.addOrganizationToCollectionIfMissing).toHaveBeenCalledWith( + organizationCollection, + ...additionalOrganizations.map(expect.objectContaining), + ); + expect(comp.organizationsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call InputDataUpload query and add missing value', () => { + const inputData: IInputData = { id: 456 }; + const sourceInputDataUpload: IInputDataUpload = { id: 31895 }; + inputData.sourceInputDataUpload = sourceInputDataUpload; + + const inputDataUploadCollection: IInputDataUpload[] = [{ id: 29460 }]; + jest.spyOn(inputDataUploadService, 'query').mockReturnValue(of(new HttpResponse({ body: inputDataUploadCollection }))); + const additionalInputDataUploads = [sourceInputDataUpload]; + const expectedCollection: IInputDataUpload[] = [...additionalInputDataUploads, ...inputDataUploadCollection]; + jest.spyOn(inputDataUploadService, 'addInputDataUploadToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(inputDataUploadService.query).toHaveBeenCalled(); + expect(inputDataUploadService.addInputDataUploadToCollectionIfMissing).toHaveBeenCalledWith( + inputDataUploadCollection, + ...additionalInputDataUploads.map(expect.objectContaining), + ); + expect(comp.inputDataUploadsSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const inputData: IInputData = { id: 456 }; + const sourceInputData: IInputData = { id: 6255 }; + inputData.sourceInputData = sourceInputData; + const variable: IVariable = { id: 17285 }; + inputData.variable = variable; + const period: IPeriod = { id: 29501 }; + inputData.period = period; + const periodVersion: IPeriodVersion = { id: 10417 }; + inputData.periodVersion = periodVersion; + const sourceUnit: IUnit = { id: 4773 }; + inputData.sourceUnit = sourceUnit; + const unit: IUnit = { id: 14051 }; + inputData.unit = unit; + const owner: IOrganization = { id: 16657 }; + inputData.owner = owner; + const sourceInputDataUpload: IInputDataUpload = { id: 3116 }; + inputData.sourceInputDataUpload = sourceInputDataUpload; + + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + expect(comp.inputDataSharedCollection).toContain(sourceInputData); + expect(comp.variablesSharedCollection).toContain(variable); + expect(comp.periodsSharedCollection).toContain(period); + expect(comp.periodVersionsSharedCollection).toContain(periodVersion); + expect(comp.unitsSharedCollection).toContain(sourceUnit); + expect(comp.unitsSharedCollection).toContain(unit); + expect(comp.organizationsSharedCollection).toContain(owner); + expect(comp.inputDataUploadsSharedCollection).toContain(sourceInputDataUpload); + expect(comp.inputData).toEqual(inputData); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputData = { id: 123 }; + jest.spyOn(inputDataFormService, 'getInputData').mockReturnValue(inputData); + jest.spyOn(inputDataService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: inputData })); + saveSubject.complete(); + + // THEN + expect(inputDataFormService.getInputData).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(inputDataService.update).toHaveBeenCalledWith(expect.objectContaining(inputData)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputData = { id: 123 }; + jest.spyOn(inputDataFormService, 'getInputData').mockReturnValue({ id: null }); + jest.spyOn(inputDataService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputData: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: inputData })); + saveSubject.complete(); + + // THEN + expect(inputDataFormService.getInputData).toHaveBeenCalled(); + expect(inputDataService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const inputData = { id: 123 }; + jest.spyOn(inputDataService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ inputData }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(inputDataService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareInputData', () => { + it('Should forward to inputDataService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(inputDataService, 'compareInputData'); + comp.compareInputData(entity, entity2); + expect(inputDataService.compareInputData).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareVariable', () => { + it('Should forward to variableService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(variableService, 'compareVariable'); + comp.compareVariable(entity, entity2); + expect(variableService.compareVariable).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('comparePeriod', () => { + it('Should forward to periodService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(periodService, 'comparePeriod'); + comp.comparePeriod(entity, entity2); + expect(periodService.comparePeriod).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('comparePeriodVersion', () => { + it('Should forward to periodVersionService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(periodVersionService, 'comparePeriodVersion'); + comp.comparePeriodVersion(entity, entity2); + expect(periodVersionService.comparePeriodVersion).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareUnit', () => { + it('Should forward to unitService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(unitService, 'compareUnit'); + comp.compareUnit(entity, entity2); + expect(unitService.compareUnit).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareOrganization', () => { + it('Should forward to organizationService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(organizationService, 'compareOrganization'); + comp.compareOrganization(entity, entity2); + expect(organizationService.compareOrganization).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareInputDataUpload', () => { + it('Should forward to inputDataUploadService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(inputDataUploadService, 'compareInputDataUpload'); + comp.compareInputDataUpload(entity, entity2); + expect(inputDataUploadService.compareInputDataUpload).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/input-data/update/input-data-update.component.ts b/src/main/webapp/app/entities/input-data/update/input-data-update.component.ts new file mode 100644 index 0000000..0f10a99 --- /dev/null +++ b/src/main/webapp/app/entities/input-data/update/input-data-update.component.ts @@ -0,0 +1,480 @@ +import { Component, inject, OnInit, ChangeDetectorRef} from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, forkJoin, tap, of } from 'rxjs'; +import { finalize, map, debounceTime } from 'rxjs/operators'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { IVariable } from 'app/entities/variable/variable.model'; +import { VariableService } from 'app/entities/variable/service/variable.service'; +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodStatus } from 'app/entities/enumerations/period-status.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { IUnit } from 'app/entities/unit/unit.model'; +import { UnitService } from 'app/entities/unit/service/unit.service'; +import { UnitTypeService } from 'app/entities/unit-type/service/unit-type.service'; +import { VariableClassTypeService } from 'app/entities/variable-class-type/service/variable-class-type.service'; +import { IVariableClass } from 'app/entities/variable-class/variable-class.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { IInputDataUpload } from 'app/entities/input-data-upload/input-data-upload.model'; +import { InputDataUploadService } from 'app/entities/input-data-upload/service/input-data-upload.service'; +import { DataSourceType } from 'app/entities/enumerations/data-source-type.model'; +import { IUnitType } from 'app/entities/unit-type/unit-type.model'; +import { InputDataService } from '../service/input-data.service'; +import { IInputData, NewInputData } from '../input-data.model'; +import { InputDataFormService, InputDataFormGroup } from './input-data-form.service'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; +/* import dayjs, { Dayjs } from 'dayjs'; */ +import { dayjs } from 'app/config/dayjs'; +import { AccountService } from 'app/core/auth/account.service'; +import { UnitConverterService } from 'app/entities/unit-converter/service/unit-converter.service'; +import { SecurityPermission } from 'app/security/security-permission.model'; +import { Resources } from 'app/security/resources.model'; +import { SecurityAction } from 'app/security/security-action.model'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +@Component({ + standalone: true, + selector: 'jhi-input-data-update', + templateUrl: './input-data-update.component.html', + styleUrl: './input-data-update.component.css', + imports: [SharedModule, FormsModule, ReactiveFormsModule, MatProgressSpinnerModule], +}) +export class InputDataUpdateComponent extends AbstractResilientEntityComponent implements OnInit { + isSaving = false; + inputData: IInputData | null = null; + dataSourceTypeValues = Object.keys(DataSourceType); + selectedVariableUnitType: IUnitType | null = null; + currentUsername: string | null = null; + variableValueHasError: boolean = false; + + /* Flags for spinner control */ + isVariableValueCalculating: boolean = false; + isLoadingVariables: boolean = false; + showSpinnerForVariableClasses: boolean = false; + + variablesSharedCollection: IVariable[] = []; + variableClassesSharedCollection: IVariable[] = []; + periodsSharedCollection: IPeriod[] = []; + unitsSharedCollection: IUnit[] = []; + organizationsSharedCollection: IOrganization[] = []; + + protected inputDataService = inject(InputDataService); + protected inputDataFormService = inject(InputDataFormService); + protected variableService = inject(VariableService); + protected variableClassTypeService = inject(VariableClassTypeService); + protected periodService = inject(PeriodService); + protected unitService = inject(UnitService); + protected unitTypeService = inject(UnitTypeService); + protected organizationService = inject(OrganizationService); + protected inputDataUploadService = inject(InputDataUploadService); + protected activatedRoute = inject(ActivatedRoute); + protected accountService = inject(AccountService); + protected unitConverterService = inject(UnitConverterService); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: InputDataFormGroup = this.inputDataFormService.createInputDataFormGroup(); + + compareInputData = (o1: IInputData | null, o2: IInputData | null): boolean => this.inputDataService.compareEntity(o1, o2); + + compareVariable = (o1: IVariable | null, o2: IVariable | null): boolean => this.variableService.compareEntity(o1, o2); + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => this.periodService.compareEntity(o1, o2); + + compareUnit = (o1: IUnit | null, o2: IUnit | null): boolean => this.unitService.compareEntity(o1, o2); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + compareInputDataUpload = (o1: IInputDataUpload | null, o2: IInputDataUpload | null): boolean => + this.inputDataUploadService.compareEntity(o1, o2); + + constructor(private cdr: ChangeDetectorRef, service: InputDataService) { super(service); } + + ngOnInit(): void { + this.accountService.identity().subscribe(account => { + this.currentUsername = account?.login ?? null; + }); + + this.activatedRoute.data.subscribe(({ inputData }) => { + this.inputData = inputData; + if (inputData) { + // Editing an existing InputData + forkJoin([ + this.loadUnitRelationshipOptions(inputData.variable.id), + this.loadVariableClassRelationshipOptions(inputData.variable.id), + this.loadVariableUnitValueType(inputData.variable) + ]).subscribe(([units, variableClasses, valueType]) => { + this.unitsSharedCollection = units; + this.variableClassesSharedCollection = variableClasses; + this.selectedVariableUnitType = valueType; + + this.updateForm(inputData); // Now update the form + this.showSpinnerForVariableClasses = false; + }); + } else { + // Creating an new InputData + this.isLoadingVariables = true; + + this.editForm.get('sourceInputDataUpload')?.disable(); + this.editForm.get('sourceInputData')?.disable(); + this.editForm.patchValue({ sourceType: DataSourceType.MANUAL }); + this.editForm.patchValue({ dataDate: dayjs() }); // Here its editable, so, dayjs() + this.editForm.patchValue({ creationDate: dayjs().format('ddd, DD MMM YYYY HH:mm:ss [GMT]') }); //format('YYYY-MM-DD') toString, because the form is read only + this.editForm.patchValue({ dataUser: this.currentUsername }); // Suggestion, the user can change the text + this.editForm.patchValue({ creationUsername: this.currentUsername }); + this.editForm.patchValue({ changeUsername: this.currentUsername }); + + // Load lists, only in create mode + this.loadPeriods(); + + // Listen to changes to the variable property + this.editForm.get('variable')?.valueChanges.subscribe((variable) => { + this.editForm.get('sourceUnit')?.setValue(null); + this.editForm.get('unit')?.setValue(null); + + if (variable) { + // Load and set BaseUnit. (Also, sugest baseUnit for the sourceUnit ) + this.variableService.queryForBaseUnit(variable.id) + .pipe(map((res: HttpResponse) => res.body as IUnit)) + .subscribe((unit: IUnit) => { + this.editForm.get('unit')?.setValue(unit); + this.editForm.get('sourceUnit')?.setValue(unit); + }); + + // Show/hide value or text properties + this.loadVariableUnitValueType(variable).subscribe((valueType) => { + this.selectedVariableUnitType = valueType; + + // Validators + const sourceValueControl = this.editForm.get('sourceValue'); + const imputedValueControl = this.editForm.get('imputedValue'); + const variableValueControl = this.editForm.get('variableValue'); + const stringValueControl = this.editForm.get('stringValue'); + sourceValueControl?.clearValidators(); + imputedValueControl?.clearValidators(); + variableValueControl?.clearValidators(); + stringValueControl?.clearValidators(); + + if (this.selectedVariableUnitType) { + switch (this.selectedVariableUnitType.valueType) { + case 'DECIMAL': + sourceValueControl?.setValidators([Validators.required]); + imputedValueControl?.setValidators([Validators.required]); + variableValueControl?.setValidators([Validators.required]); + break; + case 'STRING': + stringValueControl?.setValidators([Validators.required]); + break; + default: + console.log('Unknown fruit.'); + } + } + + // Update the validity of the control's + sourceValueControl?.updateValueAndValidity(); + imputedValueControl?.updateValueAndValidity(); + variableValueControl?.updateValueAndValidity(); + stringValueControl?.updateValueAndValidity(); + }); + + this.loadVariableClassRelationshipOptions(variable.id).subscribe((variableClasses) => { + this.variableClassesSharedCollection = variableClasses; + this.showSpinnerForVariableClasses = false; + }); + } else { + this.loadVariableClassRelationshipOptions(null).subscribe((variableClasses) => { + this.variableClassesSharedCollection = variableClasses; + this.showSpinnerForVariableClasses = false; + }); + } + + this.loadUnitsForVariable(variable).subscribe((units: IUnit[] | null) => { + if (units) { + this.unitsSharedCollection = units; + + if (units.length == 1) { + this.editForm.patchValue({ sourceUnit: this.unitsSharedCollection[0] }); + } + } + }); + }); + + // Listen to changes to the sourceValue property + this.editForm.get('sourceValue')?.valueChanges + .pipe( debounceTime(400) ) // waits 400ms after user stops typing. This prevents calling REST onType + .subscribe((value) => { this.calculateVariableValue(); }); + } + + // Listen to changes to the sourceUnit property + this.editForm.get('sourceUnit')?.valueChanges + .subscribe((value) => { this.calculateVariableValue(); }); + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + onVariableClassSelectionChange(event: Event): void { + const variableClassCodeSelectedElement = document.querySelector('#field_variableClassCode') as HTMLSelectElement; + + const selectElement = event.target as HTMLSelectElement; + const selectedText = selectElement.options[selectElement.selectedIndex].text; + + this.editForm.patchValue({variableClassName: selectedText}); + } + + save(): void { + this.isSaving = true; + const inputData = this.inputDataFormService.getInputData(this.editForm); + if (inputData.id !== null) { + this.subscribeToSaveResponse(this.inputDataService.update(inputData)); + } else { + this.subscribeToSaveResponse(this.inputDataService.create(inputData)); + } + } + + private calculateVariableValue(): void { + // To calculate, must have: + // - sourceUnit NOT null + // - sourceUnit.unitType.valueType == DECIMAL + // - sourceValue NOT null + const sourceUnit: IUnit = this.editForm.get('sourceUnit')?.value!; + const unit: IUnit = this.editForm.get('unit')?.value!; + const variable: IVariable = this.editForm.get('variable')?.value!; + const sourceValue = this.editForm.get('sourceValue')?.value; + + this.variableValueHasError = false; + this.isVariableValueCalculating = true; + + if (sourceUnit && sourceValue && unit && variable) { + // Set values + if (sourceUnit.unitType?.id == unit.unitType?.id) { + // Doesn't need convertion + this.editForm.get('imputedValue')?.setValue(sourceValue); + this.editForm.get('variableValue')?.setValue(sourceValue); + + this.isVariableValueCalculating = false; + } else { + // Needs convertion + const period: IPeriod = this.editForm.get('period')?.value!; + const variableClassCode = this.editForm.get('variableClassCode')?.value; + + this.unitConverterService.convertValue(period.id, variable.id, variableClassCode ?? null, sourceUnit.id, sourceValue) + .subscribe({ + next : (convertedValue) => { + const myConvertedValue = convertedValue.body; + + this.editForm.get('imputedValue')?.setValue(myConvertedValue); + this.editForm.get('variableValue')?.setValue(myConvertedValue); + + this.isVariableValueCalculating = false; + }, + error: (err) => { + // Optionally show a message or set fallback value + this.editForm.get('imputedValue')?.setValue(null); + this.editForm.get('variableValue')?.setValue(null); + + this.variableValueHasError = true; + this.isVariableValueCalculating = false; + /* + const variableValueField = this.editForm.get('variableValue'); + variableValueField?.setErrors({ ...(variableValueField.errors || {}), customError: 'Unable to convert the value between unit\'s' }); + variableValueField?.updateValueAndValidity(); + */ + } + }); + } + + } else { + // Clear values + this.editForm.get('imputedValue')?.setValue(null); + this.editForm.get('variableValue')?.setValue(null); + + this.isVariableValueCalculating = false; + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(inputData: IInputData): void { + this.inputData = inputData; + this.inputDataFormService.resetForm(this.editForm, inputData); + + this.variablesSharedCollection = this.variableService.addEntityToCollectionIfMissing( + this.variablesSharedCollection, + inputData.variable, + ); + this.periodsSharedCollection = this.periodService.addEntityToCollectionIfMissing( + this.periodsSharedCollection, + inputData.period, + ); + this.unitsSharedCollection = this.unitService.addEntityToCollectionIfMissing( + this.unitsSharedCollection, + inputData.sourceUnit, + inputData.unit, + ); + this.organizationsSharedCollection = this.organizationService.addEntityToCollectionIfMissing( + this.organizationsSharedCollection, + inputData.owner, + ); + + // Update the variableClassName property by simulating a the call to change event + // This is needed because the user might have changed the name of the class name in the table, and now wants to update-it + this.cdr.detectChanges(); // Forces HTML to render and update SelectList with the variableClass options. + const variableClassCodeSelectedElement = document.querySelector('#field_variableClassCode') as HTMLSelectElement; //Get select option + const variableClassNameText = variableClassCodeSelectedElement ? variableClassCodeSelectedElement.options[variableClassCodeSelectedElement.selectedIndex].text : ''; + this.editForm.patchValue({variableClassName: variableClassNameText}); + } + + protected loadUnitsForVariable(variable: IVariable|null|undefined): Observable { + if (variable) { + return this.unitService + .queryForVariable(variable.id) + .pipe(map((res: HttpResponse) => res.body ?? [])); + } else { + return of(null).pipe( + tap(() => { + this.unitsSharedCollection = []; + }) + ); + } + } + + protected loadUnitRelationshipOptions(variableId: number): Observable { + // Load units allowed fot the selected Variable + return this.unitService + .queryForVariable(variableId) + .pipe( + map((res: HttpResponse) => res.body ?? []), + map((units: IUnit[]) => + this.unitService.addEntityToCollectionIfMissing(units, this.inputData?.sourceUnit, this.inputData?.unit), + ), + ); + } + + protected loadVariableUnitValueType(variable: IVariable): Observable{ + // Load UnitValueType for the selected vaariable + if (variable.baseUnit?.unitType) { + return this.unitTypeService + .find(variable.baseUnit.unitType.id) + .pipe( + map((res: HttpResponse) => res.body ?? null) + ); + } else { + // Handle the case where no request is needed by returning an Observable + return of(null).pipe( + tap(() => { + // No variable. Clear dependante internal values. + this.selectedVariableUnitType = null; + this.variableClassesSharedCollection = []; + this.unitsSharedCollection = []; + + // Clear form dependante values + this.editForm.patchValue({ + variableClassCode: null, + variableClassName: null, + sourceUnit: null + }); + }) + ); + } + } + + protected loadVariableClassRelationshipOptions(variableId: number | null): Observable { + this.showSpinnerForVariableClasses = true; + + // Load units allowed for the selected Variable. + if (variableId) { + return this.variableClassTypeService + .queryForVariable(variableId) + .pipe(map((res: HttpResponse) => res.body ?? [])); + } else { + return of([]); + } + + } + + protected loadPeriods() { + this.periodService + .queryByState(PeriodStatus.OPENED) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe(map((periods: IPeriod[]) => this.periodService.addEntityToCollectionIfMissing(periods, this.inputData?.period))) + .subscribe((periods: IPeriod[]) => { + this.periodsSharedCollection = periods; + + if (periods.length == 1) { + this.editForm.patchValue({ period: this.periodsSharedCollection[0] }); + } + }); + } + + protected loadRelationshipsOptions(): void { + this.variableService + .queryForManualInput() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((variables: IVariable[]) => + this.variableService.addEntityToCollectionIfMissing(variables, this.inputData?.variable), + ), + ) + .subscribe((variables: IVariable[]) => { + this.variablesSharedCollection = variables; + this.isLoadingVariables = false; + }); + + this.queryOrganization() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((organizations: IOrganization[]) => + this.organizationService.addEntityToCollectionIfMissing(organizations, this.inputData?.owner), + ), + ) + .subscribe((organizations: IOrganization[]) => (this.organizationsSharedCollection = organizations)); + } + + private queryOrganization(): Observable> { + // Distinguish the organization collection, if user has access to ALL or only HIERARCHY + let permission: SecurityPermission = SecurityPermission.NONE; + if (this.inputData?.id) { + permission = this.accountService.getPermission(Resources.INPUT_DATA, SecurityAction.UPDATE); + } else { + permission = this.accountService.getPermission(Resources.INPUT_DATA, SecurityAction.CREATE); + } + + if (permission === SecurityPermission.ALL) { + // Access to all + return this.organizationService.queryByNatureForInput(OrganizationNature.ORGANIZATION) + } if (permission === SecurityPermission.HIERARCHY) { + // Only user hierarchy + return this.organizationService.queryByNatureAndHierarchyForInput(OrganizationNature.ORGANIZATION); + } else { + // NOTHING + return of(new HttpResponse({ body: [], status: 200 })); + } + } +} diff --git a/src/main/webapp/app/entities/inventory/inventory.model.ts b/src/main/webapp/app/entities/inventory/inventory.model.ts new file mode 100644 index 0000000..d4fe479 --- /dev/null +++ b/src/main/webapp/app/entities/inventory/inventory.model.ts @@ -0,0 +1,40 @@ +import dayjs from 'dayjs/esm'; +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariable } from 'app/entities/variable/variable.model'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { IPeriod } from 'app/entities/period/period.model'; +import { IUnit } from 'app/entities/unit/unit.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; + +import { DataSourceType } from 'app/entities/enumerations/data-source-type.model'; + +export interface IInventory extends IResilientEntity { + unit?: Pick | null; + owner?: Pick | null; + variableClassCode?: string | null; + variableClassName?: string | null; + variable?: Pick | null; + period?: Pick | null; + value?: number | null; + stringValue?: string | null; +} + +export type NewInventory = Omit & { id: null }; + +export type SearchInventory = Omit< + IInventory, + // Remove unwanted properties for search + 'id' | 'variable' | 'period' | 'owner'> + & + // Add specific properties for search + { + id?: number | null; + variableId?: number | null; + variableNameOrCode?: string | null; + periodId?: number | null; + ownerId?: number | null; + categoryId?: number | null; + scopeId?: number | null; + }; diff --git a/src/main/webapp/app/entities/inventory/inventory.routes.ts b/src/main/webapp/app/entities/inventory/inventory.routes.ts new file mode 100644 index 0000000..1c9e3ce --- /dev/null +++ b/src/main/webapp/app/entities/inventory/inventory.routes.ts @@ -0,0 +1,21 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { InventoryComponent } from './list/inventory.component'; +import { SecurityAction } from 'app/security/security-action.model'; +import { InventoryService } from './service/inventory.service'; +import { Resources } from 'app/security/resources.model'; + +const inventoryRoute: Routes = [ + { + path: '', + component: InventoryComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR', 'ROLE_USER'], + } + }, +]; + +export default inventoryRoute; diff --git a/src/main/webapp/app/entities/inventory/list/inventory-search-form.service.ts b/src/main/webapp/app/entities/inventory/list/inventory-search-form.service.ts new file mode 100644 index 0000000..f18b9be --- /dev/null +++ b/src/main/webapp/app/entities/inventory/list/inventory-search-form.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { SearchInventory } from '../inventory.model'; +import { SearchFormService } from 'app/entities/shared/shared-search-form.service'; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts SearchInputData, for search. in the list + */ +type InventorySearchFormGroupInput = SearchInventory; + +type InventorySearchFormDefaults = SearchInventory; + +type InventorySearchFormGroupContent = { + id: FormControl; + value: FormControl; + variableId: FormControl; + variableNameOrCode: FormControl; + periodId: FormControl; + ownerId: FormControl; + categoryId: FormControl; + scopeId: FormControl; +}; + +export type InventorySearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class InventorySearchFormService extends SearchFormService { + createInventorySearchFormGroup(inventory: InventorySearchFormGroupInput = { id: null }): InventorySearchFormGroup { + const inventoryRawValue = { + ...this.getSearchFormDefaults(), + ...inventory, + }; + return new FormGroup({ + id: new FormControl(inventoryRawValue.id), + value: new FormControl(inventoryRawValue.value), + variableId: new FormControl(inventoryRawValue.variableId), + variableNameOrCode: new FormControl(inventoryRawValue.variableNameOrCode), + ownerId: new FormControl(inventoryRawValue.ownerId), + periodId: new FormControl(inventoryRawValue.periodId, { + validators: [Validators.required], + }), + categoryId: new FormControl(inventoryRawValue.categoryId, { + validators: [Validators.required], + }), + scopeId: new FormControl(inventoryRawValue.scopeId, { + validators: [Validators.required], + }), + }); + } + + getInventory(form: InventorySearchFormGroup): SearchInventory { + return form.getRawValue() as SearchInventory; + } + + resetForm(form: InventorySearchFormGroup, inventory: InventorySearchFormGroupInput): void { + const inventoryRawValue = { ...this.getSearchFormDefaults(), ...inventory }; + form.reset( + { + ...inventoryRawValue, + id: { value: inventoryRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): InventorySearchFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/inventory/list/inventory.component.css b/src/main/webapp/app/entities/inventory/list/inventory.component.css new file mode 100644 index 0000000..a4f4c4f --- /dev/null +++ b/src/main/webapp/app/entities/inventory/list/inventory.component.css @@ -0,0 +1,43 @@ +.search-form { + display: grid; + grid-template-columns: repeat(4, 1fr); /* 4 equal columns */ + gap: 15px; + align-items: end; +} + +.form-group { + display: flex; + flex-direction: column; +} + +label { + font-weight: bold; + margin-bottom: 5px; +} + +select, button { + padding: 8px; + font-size: 16px; +} + +button { + cursor: pointer; + background-color: #007BFF; + color: white; + border: none; + padding: 10px; + font-weight: bold; +} + +button:hover { + background-color: #0056b3; +} + +.btn-refresh { + min-width: 200px; + margin: 0px; +} + +#searchForm .form-group { + width: 100%; +} diff --git a/src/main/webapp/app/entities/inventory/list/inventory.component.html b/src/main/webapp/app/entities/inventory/list/inventory.component.html new file mode 100644 index 0000000..45ed284 --- /dev/null +++ b/src/main/webapp/app/entities/inventory/list/inventory.component.html @@ -0,0 +1,166 @@ +
+

+ Activity Data + +
+ +
+

+ + + + + + {{ ' ' // Search form }} +
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+
+ + @if (inventory) { +
+ {{ ' ' // Wrap the all table in a form. This is the only way reactiveforms will work }} +
+ + + + + + + + + + + + + @for (inventory of inventory; track trackId) { + + + + + + + } + +
+
+ Variable +
+
+
+ Classe +
+
+
+ Valor +
+
+
+ Unidade +
+
+ @if (inventory.variable) { + {{ inventory.variable.name }} + } + + @if (inventory.variableClassName) { + {{ inventory.variableClassName }} + } + + @if (inventory.stringValue) { + {{ inventory.stringValue }} + } @else { + @if (inventory.value) { + {{ formatValue(inventory.value) }} + } + } + + @if (inventory.unit) { + {{ inventory.unit.symbol }} + } +
+ +
+
+ } + + @if (inventory?.length === 0) { +
+ Nenhum Inventory encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/inventory/list/inventory.component.ts b/src/main/webapp/app/entities/inventory/list/inventory.component.ts new file mode 100644 index 0000000..4d092bb --- /dev/null +++ b/src/main/webapp/app/entities/inventory/list/inventory.component.ts @@ -0,0 +1,206 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, map } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IInventory } from '../inventory.model'; +import { IPeriod } from 'app/entities/period/period.model'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { EntityArrayResponseType, InventoryService } from '../service/inventory.service'; +import { InventorySearchFormService, InventorySearchFormGroup } from './inventory-search-form.service'; +import { ResilientEnvironmentService } from 'app/resilient/resilient-environment/service/resilient-environment.service'; +import { FormatHelper } from 'app/shared/utils/format-helper'; + +@Component({ + standalone: true, + selector: 'jhi-inventory', + templateUrl: './inventory.component.html', + styleUrl: './inventory.component.css', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + MatProgressSpinnerModule, + ], +}) +export class InventoryComponent implements OnInit { + subscriptionWorkOrganization!: Subscription; // Store the subscription + subscription: Subscription | null = null; + inventory?: IInventory[]; + isLoading = false; + isExporting: boolean = false; + + scopesSharedCollection: IVariableScope[] = []; + categoriesSharedCollection: IVariableCategory[] = []; + periodsSharedCollection: IPeriod[] = []; + orgsSharedCollection: IOrganization[] = []; + + + public router = inject(Router); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + protected periodService = inject(PeriodService); + protected organizationService = inject(OrganizationService); + protected inventoryService = inject(InventoryService); + protected inventorySearchFormService = inject(InventorySearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + protected envService = inject(ResilientEnvironmentService); + + searchForm: InventorySearchFormGroup = this.inventorySearchFormService.createInventorySearchFormGroup(); + trackId = (_index: number, item: IInventory): number => this.inventoryService.getEntityIdentifier(item); + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => + this.periodService.compareEntity(o1, o2); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + compareScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + compareCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + ngOnInit(): void { + // Get the currently selected ORGANIZATION, by ResilientEnvironmentService + this.subscriptionWorkOrganization = this + .envService + .selectedWorkOrganization + /* .pipe(first()) // READS the Observable only once. Doesn't react to later changes on the subscription */ + .subscribe(org => { + if(org){ + // Set filter + this.searchForm.get('ownerId')?.setValue(org.id); + + // Clear list + this.inventory = undefined; + + // Execute search + if (this.searchForm.valid) { + this.load(); + } + } + }); + + // Listen to changes to the scope filter property + this.searchForm.get('scopeId')?.valueChanges.subscribe((scopeId) => { + this.loadCategoriesOfSelectedScope(scopeId); + this.searchForm.get('categoryId')?.setValue(null); + }); + + this.loadRelationshipsOptions(); + } + + onSearch(): void { + this.load(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + // this.inventory = this.refineData(dataFromBody); + this.inventory = dataFromBody; + } + + protected fillComponentAttributesFromResponseBody(data: IInventory[] | null): IInventory[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + }; + + const searchModel = this.inventorySearchFormService.getInventory(this.searchForm); + this.inventorySearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + + return this.inventoryService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected loadRelationshipsOptions(): void { + this.variableScopeService + .queryForData() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((scopes: IVariableScope[]) => (this.scopesSharedCollection = scopes)); + + this.periodService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((periods: IPeriod[]) => { + this.periodsSharedCollection = periods; + + // Set first Period as default. It's mandatory to select a Period + this.searchForm.get('periodId')?.setValue(this.periodsSharedCollection[0]?.id); + }); + + this.organizationService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((orgs: IOrganization[]) => (this.orgsSharedCollection = orgs)); + } + + protected loadCategoriesOfSelectedScope(scopeId: number|null|undefined): void { + if (scopeId) { + this.variableCategoryService + .queryByScopeForData(scopeId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((cats: IVariableCategory[]) => (this.categoriesSharedCollection = cats)); + } else { + this.categoriesSharedCollection = []; + } + } + + formatValue(value: number): string { + return FormatHelper.formatDecimalValue(value); + } + + export(): void { + this.isExporting = true; + const queryObject: any = { + eagerload: true, + }; + + const searchModel = this.inventorySearchFormService.getInventory(this.searchForm); + if (searchModel) { + // Apply conditions + this.inventorySearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + } + + this.inventoryService.export(queryObject).subscribe((blob: Blob) => { + const a = document.createElement('a'); + const objectUrl = URL.createObjectURL(blob); + a.href = objectUrl; + a.download = 'report.xlsx'; + a.click(); + URL.revokeObjectURL(objectUrl); + + this.isExporting = false; + }); + } +} diff --git a/src/main/webapp/app/entities/inventory/service/inventory.service.ts b/src/main/webapp/app/entities/inventory/service/inventory.service.ts new file mode 100644 index 0000000..a108ee8 --- /dev/null +++ b/src/main/webapp/app/entities/inventory/service/inventory.service.ts @@ -0,0 +1,39 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; + +import dayjs from 'dayjs/esm'; + +import { isPresent } from 'app/core/util/operators'; +import { DATE_FORMAT } from 'app/config/input.constants'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IInventory, NewInventory } from '../inventory.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class InventoryService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/inventory'); + override getResourceUrl(): string { return this.resourceUrl; } + + private readonly dateProperties: Map = new Map([ + ["dataDate", DateTypeEnum.DATE], + ["changeDate", DateTypeEnum.TIMESTAMP], + ["creationDate", DateTypeEnum.TIMESTAMP] + ]); + override getDateProperties(): Map { return this.dateProperties; } + + export(req?: any): Observable { + const options = createRequestOption(req); + + return this.http.get(`${this.resourceUrl}/export`, { params: options, responseType: 'blob' }); + } +} diff --git a/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.html b/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.html new file mode 100644 index 0000000..3b6fe20 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (metadataProperty) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.spec.ts new file mode 100644 index 0000000..199acdb --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataPropertyService } from '../service/metadata-property.service'; + +import { MetadataPropertyDeleteDialogComponent } from './metadata-property-delete-dialog.component'; + +describe('MetadataProperty Management Delete Component', () => { + let comp: MetadataPropertyDeleteDialogComponent; + let fixture: ComponentFixture; + let service: MetadataPropertyService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetadataPropertyDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(MetadataPropertyDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(MetadataPropertyDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(MetadataPropertyService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.ts b/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.ts new file mode 100644 index 0000000..8668742 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/delete/metadata-property-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IMetadataProperty } from '../metadata-property.model'; +import { MetadataPropertyService } from '../service/metadata-property.service'; + +@Component({ + standalone: true, + templateUrl: './metadata-property-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class MetadataPropertyDeleteDialogComponent { + metadataProperty?: IMetadataProperty; + + protected metadataPropertyService = inject(MetadataPropertyService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.metadataPropertyService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.html b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.html new file mode 100644 index 0000000..975ea74 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.html @@ -0,0 +1,90 @@ + +
+
+ @if (metadataProperty()) { +
+

+ Metadata Property +

+ +
+ + + + + +
+
Code
+
+ {{ metadataProperty()!.code }} +
+
Name
+
+ {{ metadataProperty()!.name }} +
+
Description
+
+ {{ metadataProperty()!.description }} +
+null
Mandatory
+
+ {{ metadataProperty()!.mandatory }} +
+
+ Pattern +
+
+ {{ metadataProperty()!.pattern }} +
+ + + +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.html.old b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.html.old new file mode 100644 index 0000000..361446a --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.html.old @@ -0,0 +1,85 @@ +
+
+ @if (metadataProperty()) { +
+

+ Metadata Property +

+ +
+ + + + + +
+
Código
+
+ {{ metadataProperty()!.id }} +
+
Code
+
+ {{ metadataProperty()!.code }} +
+
Name
+
+ {{ metadataProperty()!.name }} +
+
Description
+
+ {{ metadataProperty()!.description }} +
+
Mandatory
+
+ {{ metadataProperty()!.mandatory }} +
+
Metadata Type
+
+ {{ + { null: '', STRING: 'STRING', BOOLEAN: 'BOOLEAN', INTEGER: 'INTEGER', DECIMAL: 'DECIMAL', DATE: 'DATE' }[ + metadataProperty()!.metadataType ?? 'null' + ] + }} +
+
+ Pattern +
+
+ {{ metadataProperty()!.pattern }} +
+
+ Version +
+
+ {{ metadataProperty()!.version }} +
+
Organization Type
+
+ @for (organizationType of metadataProperty()!.organizationTypes; track $index; let last = $last) { + + {{ organizationType?.name }}{{ last ? '' : ', ' }} + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.spec.ts b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.spec.ts new file mode 100644 index 0000000..6603008 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { MetadataPropertyDetailComponent } from './metadata-property-detail.component'; + +describe('MetadataProperty Management Detail Component', () => { + let comp: MetadataPropertyDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MetadataPropertyDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: MetadataPropertyDetailComponent, + resolve: { metadataProperty: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(MetadataPropertyDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MetadataPropertyDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load metadataProperty on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', MetadataPropertyDetailComponent); + + // THEN + expect(instance.metadataProperty()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.ts b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.ts new file mode 100644 index 0000000..28956b0 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/detail/metadata-property-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IMetadataProperty } from '../metadata-property.model'; + +@Component({ + standalone: true, + selector: 'jhi-metadata-property-detail', + templateUrl: './metadata-property-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class MetadataPropertyDetailComponent { + metadataProperty = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.html b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.html new file mode 100644 index 0000000..6216c86 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.html @@ -0,0 +1,173 @@ + +
+

+ Metadata Properties + +
+ + + +
+

+ + + + + + @if (metadataProperties?.length === 0) { +
+ Nenhum Metadata Properties encontrado +
+ } + + @if (metadataProperties && metadataProperties.length > 0) { +
+ + + + + + + + + + + + + + + @for (metadataProperty of metadataProperties; track trackId) { + + + + + + + + + + + } + +
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Mandatory + + +
+
{{ metadataProperty.code }}{{ metadataProperty.name }}{{ metadataProperty.description }}{{ metadataProperty.mandatory }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.html.old b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.html.old new file mode 100644 index 0000000..ce2fa4f --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.html.old @@ -0,0 +1,164 @@ +
+

+ Metadata Properties + +
+ + + +
+

+ + + + + + @if (metadataProperties?.length === 0) { +
+ Nenhum Metadata Properties encontrado +
+ } + + @if (metadataProperties && metadataProperties.length > 0) { +
+ + + + + + + + + + + + + + + + + @for (metadataProperty of metadataProperties; track trackId) { + + + + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Mandatory + + +
+
+
+ Metadata Type + + +
+
+
+ Pattern + + +
+
+
+ Version + + +
+
+
+ Organization Type + +
+
+ {{ metadataProperty.id }} + {{ metadataProperty.code }}{{ metadataProperty.name }}{{ metadataProperty.description }}{{ metadataProperty.mandatory }} + {{ + { null: '', STRING: 'STRING', BOOLEAN: 'BOOLEAN', INTEGER: 'INTEGER', DECIMAL: 'DECIMAL', DATE: 'DATE' }[ + metadataProperty.metadataType ?? 'null' + ] + }} + {{ metadataProperty.pattern }}{{ metadataProperty.version }} + @for (organizationType of metadataProperty.organizationTypes; track $index; let last = $last) { + + {{ + organizationType.name + }}{{ last ? '' : ', ' }} + + } + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.spec.ts b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.spec.ts new file mode 100644 index 0000000..95e6e82 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../metadata-property.test-samples'; +import { MetadataPropertyService } from '../service/metadata-property.service'; + +import { MetadataPropertyComponent } from './metadata-property.component'; +import SpyInstance = jest.SpyInstance; + +describe('MetadataProperty Management Component', () => { + let comp: MetadataPropertyComponent; + let fixture: ComponentFixture; + let service: MetadataPropertyService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetadataPropertyComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(MetadataPropertyComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(MetadataPropertyComponent); + comp = fixture.componentInstance; + service = TestBed.inject(MetadataPropertyService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.metadataProperties?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to metadataPropertyService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getMetadataPropertyIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getMetadataPropertyIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.ts b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.ts new file mode 100644 index 0000000..4f47340 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/list/metadata-property.component.ts @@ -0,0 +1,121 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IMetadataProperty } from '../metadata-property.model'; +import { EntityArrayResponseType, MetadataPropertyService } from '../service/metadata-property.service'; +import { MetadataPropertyDeleteDialogComponent } from '../delete/metadata-property-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-metadata-property', + templateUrl: './metadata-property.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class MetadataPropertyComponent implements OnInit { + subscription: Subscription | null = null; + metadataProperties?: IMetadataProperty[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected metadataPropertyService = inject(MetadataPropertyService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IMetadataProperty): number => this.metadataPropertyService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.metadataProperties || this.metadataProperties.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(metadataProperty: IMetadataProperty): void { + const modalRef = this.modalService.open(MetadataPropertyDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.metadataProperty = metadataProperty; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.metadataProperties = this.refineData(dataFromBody); + } + + protected refineData(data: IMetadataProperty[]): IMetadataProperty[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IMetadataProperty[] | null): IMetadataProperty[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.metadataPropertyService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/metadata-property/metadata-property.model.ts b/src/main/webapp/app/entities/metadata-property/metadata-property.model.ts new file mode 100644 index 0000000..9f3fe8c --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/metadata-property.model.ts @@ -0,0 +1,16 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; +import { MetadataType } from 'app/entities/enumerations/metadata-type.model'; + +export interface IMetadataProperty extends IResilientEntity { + code?: string | null; + name?: string | null; + description?: string | null; + mandatory?: boolean | null; + metadataType?: keyof typeof MetadataType | null; + pattern?: string | null; + organizationTypes?: Pick[] | null; +} + +export type NewMetadataProperty = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/metadata-property/metadata-property.routes.ts b/src/main/webapp/app/entities/metadata-property/metadata-property.routes.ts new file mode 100644 index 0000000..bb68990 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/metadata-property.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { MetadataPropertyComponent } from './list/metadata-property.component'; +import { MetadataPropertyDetailComponent } from './detail/metadata-property-detail.component'; +import { MetadataPropertyUpdateComponent } from './update/metadata-property-update.component'; +import MetadataPropertyResolve from './route/metadata-property-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { MetadataPropertyService } from './service/metadata-property.service'; +import { Resources } from 'app/security/resources.model'; + +const metadataPropertyRoute: Routes = [ + { + path: '', + component: MetadataPropertyComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: MetadataPropertyService, + resource: Resources.METADATA_PROPERTY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: MetadataPropertyDetailComponent, + resolve: { + metadataProperty: MetadataPropertyResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: MetadataPropertyService, + resource: Resources.METADATA_PROPERTY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: MetadataPropertyUpdateComponent, + resolve: { + metadataProperty: MetadataPropertyResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: MetadataPropertyService, + resource: Resources.METADATA_PROPERTY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: MetadataPropertyUpdateComponent, + resolve: { + metadataProperty: MetadataPropertyResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: MetadataPropertyService, + resource: Resources.METADATA_PROPERTY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default metadataPropertyRoute; diff --git a/src/main/webapp/app/entities/metadata-property/metadata-property.test-samples.ts b/src/main/webapp/app/entities/metadata-property/metadata-property.test-samples.ts new file mode 100644 index 0000000..4510be1 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/metadata-property.test-samples.ts @@ -0,0 +1,39 @@ +import { IMetadataProperty, NewMetadataProperty } from './metadata-property.model'; + +export const sampleWithRequiredData: IMetadataProperty = { + id: 14053, + code: 'nicely', + name: 'pro excepting jolt', + metadataType: 'DECIMAL', +}; + +export const sampleWithPartialData: IMetadataProperty = { + id: 10012, + code: 'gadzooks', + name: 'blah physical aboard', + metadataType: 'STRING', + version: 29907, +}; + +export const sampleWithFullData: IMetadataProperty = { + id: 29184, + code: 'past jewel', + name: 'dearly to', + description: 'once whenever', + mandatory: false, + metadataType: 'BOOLEAN', + pattern: 'add woot while', + version: 30970, +}; + +export const sampleWithNewData: NewMetadataProperty = { + code: 'archives', + name: 'psst ouch', + metadataType: 'DECIMAL', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/metadata-property/route/metadata-property-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/metadata-property/route/metadata-property-routing-resolve.service.spec.ts new file mode 100644 index 0000000..6bccfe0 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/route/metadata-property-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IMetadataProperty } from '../metadata-property.model'; +import { MetadataPropertyService } from '../service/metadata-property.service'; + +import metadataPropertyResolve from './metadata-property-routing-resolve.service'; + +describe('MetadataProperty routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: MetadataPropertyService; + let resultMetadataProperty: IMetadataProperty | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(MetadataPropertyService); + resultMetadataProperty = undefined; + }); + + describe('resolve', () => { + it('should return IMetadataProperty returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + metadataPropertyResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultMetadataProperty = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultMetadataProperty).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + metadataPropertyResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultMetadataProperty = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultMetadataProperty).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + metadataPropertyResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultMetadataProperty = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultMetadataProperty).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-property/route/metadata-property-routing-resolve.service.ts b/src/main/webapp/app/entities/metadata-property/route/metadata-property-routing-resolve.service.ts new file mode 100644 index 0000000..390e561 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/route/metadata-property-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IMetadataProperty } from '../metadata-property.model'; +import { MetadataPropertyService } from '../service/metadata-property.service'; + +const metadataPropertyResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(MetadataPropertyService) + .find(id) + .pipe( + mergeMap((metadataProperty: HttpResponse) => { + if (metadataProperty.body) { + return of(metadataProperty.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default metadataPropertyResolve; diff --git a/src/main/webapp/app/entities/metadata-property/service/metadata-property.service.spec.ts b/src/main/webapp/app/entities/metadata-property/service/metadata-property.service.spec.ts new file mode 100644 index 0000000..cd9a90a --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/service/metadata-property.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IMetadataProperty } from '../metadata-property.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../metadata-property.test-samples'; + +import { MetadataPropertyService } from './metadata-property.service'; + +const requireRestSample: IMetadataProperty = { + ...sampleWithRequiredData, +}; + +describe('MetadataProperty Service', () => { + let service: MetadataPropertyService; + let httpMock: HttpTestingController; + let expectedResult: IMetadataProperty | IMetadataProperty[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(MetadataPropertyService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a MetadataProperty', () => { + const metadataProperty = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(metadataProperty).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a MetadataProperty', () => { + const metadataProperty = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(metadataProperty).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a MetadataProperty', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of MetadataProperty', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a MetadataProperty', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addMetadataPropertyToCollectionIfMissing', () => { + it('should add a MetadataProperty to an empty array', () => { + const metadataProperty: IMetadataProperty = sampleWithRequiredData; + expectedResult = service.addMetadataPropertyToCollectionIfMissing([], metadataProperty); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(metadataProperty); + }); + + it('should not add a MetadataProperty to an array that contains it', () => { + const metadataProperty: IMetadataProperty = sampleWithRequiredData; + const metadataPropertyCollection: IMetadataProperty[] = [ + { + ...metadataProperty, + }, + sampleWithPartialData, + ]; + expectedResult = service.addMetadataPropertyToCollectionIfMissing(metadataPropertyCollection, metadataProperty); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a MetadataProperty to an array that doesn't contain it", () => { + const metadataProperty: IMetadataProperty = sampleWithRequiredData; + const metadataPropertyCollection: IMetadataProperty[] = [sampleWithPartialData]; + expectedResult = service.addMetadataPropertyToCollectionIfMissing(metadataPropertyCollection, metadataProperty); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(metadataProperty); + }); + + it('should add only unique MetadataProperty to an array', () => { + const metadataPropertyArray: IMetadataProperty[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const metadataPropertyCollection: IMetadataProperty[] = [sampleWithRequiredData]; + expectedResult = service.addMetadataPropertyToCollectionIfMissing(metadataPropertyCollection, ...metadataPropertyArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const metadataProperty: IMetadataProperty = sampleWithRequiredData; + const metadataProperty2: IMetadataProperty = sampleWithPartialData; + expectedResult = service.addMetadataPropertyToCollectionIfMissing([], metadataProperty, metadataProperty2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(metadataProperty); + expect(expectedResult).toContain(metadataProperty2); + }); + + it('should accept null and undefined values', () => { + const metadataProperty: IMetadataProperty = sampleWithRequiredData; + expectedResult = service.addMetadataPropertyToCollectionIfMissing([], null, metadataProperty, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(metadataProperty); + }); + + it('should return initial array if no MetadataProperty is added', () => { + const metadataPropertyCollection: IMetadataProperty[] = [sampleWithRequiredData]; + expectedResult = service.addMetadataPropertyToCollectionIfMissing(metadataPropertyCollection, undefined, null); + expect(expectedResult).toEqual(metadataPropertyCollection); + }); + }); + + describe('compareMetadataProperty', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareMetadataProperty(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareMetadataProperty(entity1, entity2); + const compareResult2 = service.compareMetadataProperty(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareMetadataProperty(entity1, entity2); + const compareResult2 = service.compareMetadataProperty(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareMetadataProperty(entity1, entity2); + const compareResult2 = service.compareMetadataProperty(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-property/service/metadata-property.service.ts b/src/main/webapp/app/entities/metadata-property/service/metadata-property.service.ts new file mode 100644 index 0000000..9c2de9e --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/service/metadata-property.service.ts @@ -0,0 +1,23 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IMetadataProperty, NewMetadataProperty } from '../metadata-property.model'; + +export type PartialUpdateMetadataProperty = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class MetadataPropertyService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/metadata-properties'); + getResourceUrl(): string { return this.resourceUrl; } +} diff --git a/src/main/webapp/app/entities/metadata-property/update/metadata-property-form.service.spec.ts b/src/main/webapp/app/entities/metadata-property/update/metadata-property-form.service.spec.ts new file mode 100644 index 0000000..0afe56c --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/update/metadata-property-form.service.spec.ts @@ -0,0 +1,100 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../metadata-property.test-samples'; + +import { MetadataPropertyFormService } from './metadata-property-form.service'; + +describe('MetadataProperty Form Service', () => { + let service: MetadataPropertyFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MetadataPropertyFormService); + }); + + describe('Service methods', () => { + describe('createMetadataPropertyFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createMetadataPropertyFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + mandatory: expect.any(Object), + metadataType: expect.any(Object), + pattern: expect.any(Object), + version: expect.any(Object), + organizationTypes: expect.any(Object), + }), + ); + }); + + it('passing IMetadataProperty should create a new form with FormGroup', () => { + const formGroup = service.createMetadataPropertyFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + mandatory: expect.any(Object), + metadataType: expect.any(Object), + pattern: expect.any(Object), + version: expect.any(Object), + organizationTypes: expect.any(Object), + }), + ); + }); + }); + + describe('getMetadataProperty', () => { + it('should return NewMetadataProperty for default MetadataProperty initial value', () => { + const formGroup = service.createMetadataPropertyFormGroup(sampleWithNewData); + + const metadataProperty = service.getMetadataProperty(formGroup) as any; + + expect(metadataProperty).toMatchObject(sampleWithNewData); + }); + + it('should return NewMetadataProperty for empty MetadataProperty initial value', () => { + const formGroup = service.createMetadataPropertyFormGroup(); + + const metadataProperty = service.getMetadataProperty(formGroup) as any; + + expect(metadataProperty).toMatchObject({}); + }); + + it('should return IMetadataProperty', () => { + const formGroup = service.createMetadataPropertyFormGroup(sampleWithRequiredData); + + const metadataProperty = service.getMetadataProperty(formGroup) as any; + + expect(metadataProperty).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IMetadataProperty should not enable id FormControl', () => { + const formGroup = service.createMetadataPropertyFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewMetadataProperty should disable id FormControl', () => { + const formGroup = service.createMetadataPropertyFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-property/update/metadata-property-form.service.ts b/src/main/webapp/app/entities/metadata-property/update/metadata-property-form.service.ts new file mode 100644 index 0000000..3a68960 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/update/metadata-property-form.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IMetadataProperty, NewMetadataProperty } from '../metadata-property.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IMetadataProperty for edit and NewMetadataPropertyFormGroupInput for create. + */ +type MetadataPropertyFormGroupInput = IMetadataProperty | PartialWithRequiredKeyOf; + +type MetadataPropertyFormDefaults = Pick; + +type MetadataPropertyFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + description: FormControl; + mandatory: FormControl; + metadataType: FormControl; + pattern: FormControl; + version: FormControl; + organizationTypes: FormControl; +}; + +export type MetadataPropertyFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class MetadataPropertyFormService { + createMetadataPropertyFormGroup(metadataProperty: MetadataPropertyFormGroupInput = { id: null }): MetadataPropertyFormGroup { + const metadataPropertyRawValue = { + ...this.getFormDefaults(), + ...metadataProperty, + }; + return new FormGroup({ + id: new FormControl( + { value: metadataPropertyRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(metadataPropertyRawValue.code, { + validators: [Validators.required, Validators.minLength(3)], + }), + name: new FormControl(metadataPropertyRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(metadataPropertyRawValue.description), + mandatory: new FormControl(metadataPropertyRawValue.mandatory), + metadataType: new FormControl(metadataPropertyRawValue.metadataType, { + validators: [Validators.required], + }), + pattern: new FormControl(metadataPropertyRawValue.pattern), + version: new FormControl(metadataPropertyRawValue.version), + organizationTypes: new FormControl(metadataPropertyRawValue.organizationTypes ?? []), + }); + } + + getMetadataProperty(form: MetadataPropertyFormGroup): IMetadataProperty | NewMetadataProperty { + return form.getRawValue() as IMetadataProperty | NewMetadataProperty; + } + + resetForm(form: MetadataPropertyFormGroup, metadataProperty: MetadataPropertyFormGroupInput): void { + const metadataPropertyRawValue = { ...this.getFormDefaults(), ...metadataProperty }; + form.reset( + { + ...metadataPropertyRawValue, + id: { value: metadataPropertyRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): MetadataPropertyFormDefaults { + return { + id: null, + mandatory: false, + organizationTypes: [], + }; + } +} diff --git a/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.html b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.html new file mode 100644 index 0000000..2f5531e --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.html @@ -0,0 +1,143 @@ + +
+
+
+

+ Criar ou editar Metadata Property +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + +
+null
+ + +
+
+ + +
+ + + +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.html.old b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.html.old new file mode 100644 index 0000000..94bfb58 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.html.old @@ -0,0 +1,142 @@ +
+
+
+

+ Criar ou editar Metadata Property +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+ +
+ + +
+ +
+ + + @if (editForm.get('metadataType')!.invalid && (editForm.get('metadataType')!.dirty || editForm.get('metadataType')!.touched)) { +
+ @if (editForm.get('metadataType')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.spec.ts b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.spec.ts new file mode 100644 index 0000000..91547e6 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.spec.ts @@ -0,0 +1,164 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; +import { OrganizationTypeService } from 'app/entities/organization-type/service/organization-type.service'; +import { MetadataPropertyService } from '../service/metadata-property.service'; +import { IMetadataProperty } from '../metadata-property.model'; +import { MetadataPropertyFormService } from './metadata-property-form.service'; + +import { MetadataPropertyUpdateComponent } from './metadata-property-update.component'; + +describe('MetadataProperty Management Update Component', () => { + let comp: MetadataPropertyUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let metadataPropertyFormService: MetadataPropertyFormService; + let metadataPropertyService: MetadataPropertyService; + let organizationTypeService: OrganizationTypeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetadataPropertyUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(MetadataPropertyUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(MetadataPropertyUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + metadataPropertyFormService = TestBed.inject(MetadataPropertyFormService); + metadataPropertyService = TestBed.inject(MetadataPropertyService); + organizationTypeService = TestBed.inject(OrganizationTypeService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call OrganizationType query and add missing value', () => { + const metadataProperty: IMetadataProperty = { id: 456 }; + const organizationTypes: IOrganizationType[] = [{ id: 25562 }]; + metadataProperty.organizationTypes = organizationTypes; + + const organizationTypeCollection: IOrganizationType[] = [{ id: 32124 }]; + jest.spyOn(organizationTypeService, 'query').mockReturnValue(of(new HttpResponse({ body: organizationTypeCollection }))); + const additionalOrganizationTypes = [...organizationTypes]; + const expectedCollection: IOrganizationType[] = [...additionalOrganizationTypes, ...organizationTypeCollection]; + jest.spyOn(organizationTypeService, 'addOrganizationTypeToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ metadataProperty }); + comp.ngOnInit(); + + expect(organizationTypeService.query).toHaveBeenCalled(); + expect(organizationTypeService.addOrganizationTypeToCollectionIfMissing).toHaveBeenCalledWith( + organizationTypeCollection, + ...additionalOrganizationTypes.map(expect.objectContaining), + ); + expect(comp.organizationTypesSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const metadataProperty: IMetadataProperty = { id: 456 }; + const organizationType: IOrganizationType = { id: 15695 }; + metadataProperty.organizationTypes = [organizationType]; + + activatedRoute.data = of({ metadataProperty }); + comp.ngOnInit(); + + expect(comp.organizationTypesSharedCollection).toContain(organizationType); + expect(comp.metadataProperty).toEqual(metadataProperty); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const metadataProperty = { id: 123 }; + jest.spyOn(metadataPropertyFormService, 'getMetadataProperty').mockReturnValue(metadataProperty); + jest.spyOn(metadataPropertyService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ metadataProperty }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: metadataProperty })); + saveSubject.complete(); + + // THEN + expect(metadataPropertyFormService.getMetadataProperty).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(metadataPropertyService.update).toHaveBeenCalledWith(expect.objectContaining(metadataProperty)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const metadataProperty = { id: 123 }; + jest.spyOn(metadataPropertyFormService, 'getMetadataProperty').mockReturnValue({ id: null }); + jest.spyOn(metadataPropertyService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ metadataProperty: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: metadataProperty })); + saveSubject.complete(); + + // THEN + expect(metadataPropertyFormService.getMetadataProperty).toHaveBeenCalled(); + expect(metadataPropertyService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const metadataProperty = { id: 123 }; + jest.spyOn(metadataPropertyService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ metadataProperty }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(metadataPropertyService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareOrganizationType', () => { + it('Should forward to organizationTypeService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(organizationTypeService, 'compareOrganizationType'); + comp.compareOrganizationType(entity, entity2); + expect(organizationTypeService.compareOrganizationType).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.ts b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.ts new file mode 100644 index 0000000..45f7b5c --- /dev/null +++ b/src/main/webapp/app/entities/metadata-property/update/metadata-property-update.component.ts @@ -0,0 +1,109 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; +import { OrganizationTypeService } from 'app/entities/organization-type/service/organization-type.service'; +import { MetadataType } from 'app/entities/enumerations/metadata-type.model'; +import { MetadataPropertyService } from '../service/metadata-property.service'; +import { IMetadataProperty } from '../metadata-property.model'; +import { MetadataPropertyFormService, MetadataPropertyFormGroup } from './metadata-property-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-metadata-property-update', + templateUrl: './metadata-property-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class MetadataPropertyUpdateComponent implements OnInit { + isSaving = false; + metadataProperty: IMetadataProperty | null = null; + metadataTypeValues = Object.keys(MetadataType); + + organizationTypesSharedCollection: IOrganizationType[] = []; + + protected metadataPropertyService = inject(MetadataPropertyService); + protected metadataPropertyFormService = inject(MetadataPropertyFormService); + protected organizationTypeService = inject(OrganizationTypeService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: MetadataPropertyFormGroup = this.metadataPropertyFormService.createMetadataPropertyFormGroup(); + + compareOrganizationType = (o1: IOrganizationType | null, o2: IOrganizationType | null): boolean => + this.organizationTypeService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ metadataProperty }) => { + this.metadataProperty = metadataProperty; + if (metadataProperty) { + this.updateForm(metadataProperty); + } + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const metadataProperty = this.metadataPropertyFormService.getMetadataProperty(this.editForm); + if (metadataProperty.id !== null) { + this.subscribeToSaveResponse(this.metadataPropertyService.update(metadataProperty)); + } else { + this.subscribeToSaveResponse(this.metadataPropertyService.create(metadataProperty)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(metadataProperty: IMetadataProperty): void { + this.metadataProperty = metadataProperty; + this.metadataPropertyFormService.resetForm(this.editForm, metadataProperty); + + this.organizationTypesSharedCollection = this.organizationTypeService.addEntityToCollectionIfMissing( + this.organizationTypesSharedCollection, + ...(metadataProperty.organizationTypes ?? []), + ); + } + + protected loadRelationshipsOptions(): void { + this.organizationTypeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((organizationTypes: IOrganizationType[]) => + this.organizationTypeService.addEntityToCollectionIfMissing( + organizationTypes, + ...(this.metadataProperty?.organizationTypes ?? []), + ), + ), + ) + .subscribe((organizationTypes: IOrganizationType[]) => (this.organizationTypesSharedCollection = organizationTypes)); + } +} diff --git a/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.html b/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.html new file mode 100644 index 0000000..ff7bc0d --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (metadataValue) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.spec.ts new file mode 100644 index 0000000..bca29e6 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataValueService } from '../service/metadata-value.service'; + +import { MetadataValueDeleteDialogComponent } from './metadata-value-delete-dialog.component'; + +describe('MetadataValue Management Delete Component', () => { + let comp: MetadataValueDeleteDialogComponent; + let fixture: ComponentFixture; + let service: MetadataValueService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetadataValueDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(MetadataValueDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(MetadataValueDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(MetadataValueService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.ts b/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.ts new file mode 100644 index 0000000..1a2abf1 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/delete/metadata-value-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IMetadataValue } from '../metadata-value.model'; +import { MetadataValueService } from '../service/metadata-value.service'; + +@Component({ + standalone: true, + templateUrl: './metadata-value-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class MetadataValueDeleteDialogComponent { + metadataValue?: IMetadataValue; + + protected metadataValueService = inject(MetadataValueService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.metadataValueService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.html b/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.html new file mode 100644 index 0000000..eea533e --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.html @@ -0,0 +1,76 @@ +
+
+ @if (metadataValue()) { +
+

Metadata Value

+ +
+ + + + + +
+
Código
+
+ {{ metadataValue()!.id }} +
+
+ Metadata Property Code +
+
+ {{ metadataValue()!.metadataPropertyCode }} +
+
+ Target Domain Key +
+
+ {{ metadataValue()!.targetDomainKey }} +
+
+ Target Domain Id +
+
+ {{ metadataValue()!.targetDomainId }} +
+
+ Value +
+
+ {{ metadataValue()!.value }} +
+
+ Version +
+
+ {{ metadataValue()!.version }} +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.spec.ts b/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.spec.ts new file mode 100644 index 0000000..35290d5 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { MetadataValueDetailComponent } from './metadata-value-detail.component'; + +describe('MetadataValue Management Detail Component', () => { + let comp: MetadataValueDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MetadataValueDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: MetadataValueDetailComponent, + resolve: { metadataValue: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(MetadataValueDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MetadataValueDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load metadataValue on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', MetadataValueDetailComponent); + + // THEN + expect(instance.metadataValue()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.ts b/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.ts new file mode 100644 index 0000000..1478c5a --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/detail/metadata-value-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IMetadataValue } from '../metadata-value.model'; + +@Component({ + standalone: true, + selector: 'jhi-metadata-value-detail', + templateUrl: './metadata-value-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class MetadataValueDetailComponent { + metadataValue = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.html b/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.html new file mode 100644 index 0000000..8cb12f3 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.html @@ -0,0 +1,118 @@ +
+

+ Metadata Values + +
+ + + +
+

+ + + + + + @if (metadataValues?.length === 0) { +
+ Nenhum Metadata Values encontrado +
+ } + + @if (metadataValues && metadataValues.length > 0) { +
+ + + + + + + + + + + + + + @for (metadataValue of metadataValues; track trackId) { + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Metadata Property Code + + +
+
+
+ Target Domain Key + + +
+
+
+ Target Domain Id + + +
+
+
+ Value + + +
+
+
+ Version + + +
+
+ {{ metadataValue.id }} + {{ metadataValue.metadataPropertyCode }}{{ metadataValue.targetDomainKey }}{{ metadataValue.targetDomainId }}{{ metadataValue.value }}{{ metadataValue.version }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.spec.ts b/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.spec.ts new file mode 100644 index 0000000..2ebb0f4 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../metadata-value.test-samples'; +import { MetadataValueService } from '../service/metadata-value.service'; + +import { MetadataValueComponent } from './metadata-value.component'; +import SpyInstance = jest.SpyInstance; + +describe('MetadataValue Management Component', () => { + let comp: MetadataValueComponent; + let fixture: ComponentFixture; + let service: MetadataValueService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetadataValueComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(MetadataValueComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(MetadataValueComponent); + comp = fixture.componentInstance; + service = TestBed.inject(MetadataValueService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.metadataValues?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to metadataValueService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getMetadataValueIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getMetadataValueIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.ts b/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.ts new file mode 100644 index 0000000..2e41266 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/list/metadata-value.component.ts @@ -0,0 +1,121 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IMetadataValue } from '../metadata-value.model'; +import { EntityArrayResponseType, MetadataValueService } from '../service/metadata-value.service'; +import { MetadataValueDeleteDialogComponent } from '../delete/metadata-value-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-metadata-value', + templateUrl: './metadata-value.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class MetadataValueComponent implements OnInit { + subscription: Subscription | null = null; + metadataValues?: IMetadataValue[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected metadataValueService = inject(MetadataValueService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IMetadataValue): number => this.metadataValueService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.metadataValues || this.metadataValues.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(metadataValue: IMetadataValue): void { + const modalRef = this.modalService.open(MetadataValueDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.metadataValue = metadataValue; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.metadataValues = this.refineData(dataFromBody); + } + + protected refineData(data: IMetadataValue[]): IMetadataValue[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IMetadataValue[] | null): IMetadataValue[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.metadataValueService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/metadata-value/metadata-value.model.ts b/src/main/webapp/app/entities/metadata-value/metadata-value.model.ts new file mode 100644 index 0000000..8b2a6e0 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/metadata-value.model.ts @@ -0,0 +1,10 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +export interface IMetadataValue extends IResilientEntity { + metadataPropertyCode?: string | null; + targetDomainKey?: string | null; + targetDomainId?: number | null; + value?: string | null; +} + +export type NewMetadataValue = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/metadata-value/metadata-value.routes.ts b/src/main/webapp/app/entities/metadata-value/metadata-value.routes.ts new file mode 100644 index 0000000..f912e2c --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/metadata-value.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { MetadataValueComponent } from './list/metadata-value.component'; +import { MetadataValueDetailComponent } from './detail/metadata-value-detail.component'; +import { MetadataValueUpdateComponent } from './update/metadata-value-update.component'; +import MetadataValueResolve from './route/metadata-value-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { MetadataValueService } from './service/metadata-value.service'; +import { Resources } from 'app/security/resources.model'; + +const metadataValueRoute: Routes = [ + { + path: '', + component: MetadataValueComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: MetadataValueService, + resource: Resources.METADATA_VALUE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: MetadataValueDetailComponent, + resolve: { + metadataValue: MetadataValueResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: MetadataValueService, + resource: Resources.METADATA_VALUE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: MetadataValueUpdateComponent, + resolve: { + metadataValue: MetadataValueResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: MetadataValueService, + resource: Resources.METADATA_VALUE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: MetadataValueUpdateComponent, + resolve: { + metadataValue: MetadataValueResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: MetadataValueService, + resource: Resources.METADATA_VALUE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default metadataValueRoute; diff --git a/src/main/webapp/app/entities/metadata-value/metadata-value.test-samples.ts b/src/main/webapp/app/entities/metadata-value/metadata-value.test-samples.ts new file mode 100644 index 0000000..f231e81 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/metadata-value.test-samples.ts @@ -0,0 +1,36 @@ +import { IMetadataValue, NewMetadataValue } from './metadata-value.model'; + +export const sampleWithRequiredData: IMetadataValue = { + id: 16623, + metadataPropertyCode: 'rebut supposing joint', + targetDomainKey: 'idealistic', + targetDomainId: 11250, +}; + +export const sampleWithPartialData: IMetadataValue = { + id: 32048, + metadataPropertyCode: 'afore', + targetDomainKey: 'ex-husband', + targetDomainId: 3255, +}; + +export const sampleWithFullData: IMetadataValue = { + id: 19144, + metadataPropertyCode: 'backlight giant', + targetDomainKey: 'flat inasmuch while', + targetDomainId: 28828, + value: 'between bath apropos', + version: 28854, +}; + +export const sampleWithNewData: NewMetadataValue = { + metadataPropertyCode: 'vacantly about', + targetDomainKey: 'format afore wide', + targetDomainId: 4274, + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/metadata-value/route/metadata-value-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/metadata-value/route/metadata-value-routing-resolve.service.spec.ts new file mode 100644 index 0000000..0cc9626 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/route/metadata-value-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IMetadataValue } from '../metadata-value.model'; +import { MetadataValueService } from '../service/metadata-value.service'; + +import metadataValueResolve from './metadata-value-routing-resolve.service'; + +describe('MetadataValue routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: MetadataValueService; + let resultMetadataValue: IMetadataValue | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(MetadataValueService); + resultMetadataValue = undefined; + }); + + describe('resolve', () => { + it('should return IMetadataValue returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + metadataValueResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultMetadataValue = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultMetadataValue).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + metadataValueResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultMetadataValue = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultMetadataValue).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + metadataValueResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultMetadataValue = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultMetadataValue).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-value/route/metadata-value-routing-resolve.service.ts b/src/main/webapp/app/entities/metadata-value/route/metadata-value-routing-resolve.service.ts new file mode 100644 index 0000000..2be3bd9 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/route/metadata-value-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IMetadataValue } from '../metadata-value.model'; +import { MetadataValueService } from '../service/metadata-value.service'; + +const metadataValueResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(MetadataValueService) + .find(id) + .pipe( + mergeMap((metadataValue: HttpResponse) => { + if (metadataValue.body) { + return of(metadataValue.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default metadataValueResolve; diff --git a/src/main/webapp/app/entities/metadata-value/service/metadata-value.service.spec.ts b/src/main/webapp/app/entities/metadata-value/service/metadata-value.service.spec.ts new file mode 100644 index 0000000..ce797fb --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/service/metadata-value.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IMetadataValue } from '../metadata-value.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../metadata-value.test-samples'; + +import { MetadataValueService } from './metadata-value.service'; + +const requireRestSample: IMetadataValue = { + ...sampleWithRequiredData, +}; + +describe('MetadataValue Service', () => { + let service: MetadataValueService; + let httpMock: HttpTestingController; + let expectedResult: IMetadataValue | IMetadataValue[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(MetadataValueService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a MetadataValue', () => { + const metadataValue = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(metadataValue).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a MetadataValue', () => { + const metadataValue = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(metadataValue).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a MetadataValue', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of MetadataValue', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a MetadataValue', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addMetadataValueToCollectionIfMissing', () => { + it('should add a MetadataValue to an empty array', () => { + const metadataValue: IMetadataValue = sampleWithRequiredData; + expectedResult = service.addMetadataValueToCollectionIfMissing([], metadataValue); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(metadataValue); + }); + + it('should not add a MetadataValue to an array that contains it', () => { + const metadataValue: IMetadataValue = sampleWithRequiredData; + const metadataValueCollection: IMetadataValue[] = [ + { + ...metadataValue, + }, + sampleWithPartialData, + ]; + expectedResult = service.addMetadataValueToCollectionIfMissing(metadataValueCollection, metadataValue); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a MetadataValue to an array that doesn't contain it", () => { + const metadataValue: IMetadataValue = sampleWithRequiredData; + const metadataValueCollection: IMetadataValue[] = [sampleWithPartialData]; + expectedResult = service.addMetadataValueToCollectionIfMissing(metadataValueCollection, metadataValue); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(metadataValue); + }); + + it('should add only unique MetadataValue to an array', () => { + const metadataValueArray: IMetadataValue[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const metadataValueCollection: IMetadataValue[] = [sampleWithRequiredData]; + expectedResult = service.addMetadataValueToCollectionIfMissing(metadataValueCollection, ...metadataValueArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const metadataValue: IMetadataValue = sampleWithRequiredData; + const metadataValue2: IMetadataValue = sampleWithPartialData; + expectedResult = service.addMetadataValueToCollectionIfMissing([], metadataValue, metadataValue2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(metadataValue); + expect(expectedResult).toContain(metadataValue2); + }); + + it('should accept null and undefined values', () => { + const metadataValue: IMetadataValue = sampleWithRequiredData; + expectedResult = service.addMetadataValueToCollectionIfMissing([], null, metadataValue, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(metadataValue); + }); + + it('should return initial array if no MetadataValue is added', () => { + const metadataValueCollection: IMetadataValue[] = [sampleWithRequiredData]; + expectedResult = service.addMetadataValueToCollectionIfMissing(metadataValueCollection, undefined, null); + expect(expectedResult).toEqual(metadataValueCollection); + }); + }); + + describe('compareMetadataValue', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareMetadataValue(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareMetadataValue(entity1, entity2); + const compareResult2 = service.compareMetadataValue(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareMetadataValue(entity1, entity2); + const compareResult2 = service.compareMetadataValue(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareMetadataValue(entity1, entity2); + const compareResult2 = service.compareMetadataValue(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-value/service/metadata-value.service.ts b/src/main/webapp/app/entities/metadata-value/service/metadata-value.service.ts new file mode 100644 index 0000000..ee66f6e --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/service/metadata-value.service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IMetadataValue, NewMetadataValue } from '../metadata-value.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class MetadataValueService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/metadata-values'); + getResourceUrl(): string { return this.resourceUrl; } +} diff --git a/src/main/webapp/app/entities/metadata-value/update/metadata-value-form.service.spec.ts b/src/main/webapp/app/entities/metadata-value/update/metadata-value-form.service.spec.ts new file mode 100644 index 0000000..780cf42 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/update/metadata-value-form.service.spec.ts @@ -0,0 +1,94 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../metadata-value.test-samples'; + +import { MetadataValueFormService } from './metadata-value-form.service'; + +describe('MetadataValue Form Service', () => { + let service: MetadataValueFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MetadataValueFormService); + }); + + describe('Service methods', () => { + describe('createMetadataValueFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createMetadataValueFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + metadataPropertyCode: expect.any(Object), + targetDomainKey: expect.any(Object), + targetDomainId: expect.any(Object), + value: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + + it('passing IMetadataValue should create a new form with FormGroup', () => { + const formGroup = service.createMetadataValueFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + metadataPropertyCode: expect.any(Object), + targetDomainKey: expect.any(Object), + targetDomainId: expect.any(Object), + value: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + }); + + describe('getMetadataValue', () => { + it('should return NewMetadataValue for default MetadataValue initial value', () => { + const formGroup = service.createMetadataValueFormGroup(sampleWithNewData); + + const metadataValue = service.getMetadataValue(formGroup) as any; + + expect(metadataValue).toMatchObject(sampleWithNewData); + }); + + it('should return NewMetadataValue for empty MetadataValue initial value', () => { + const formGroup = service.createMetadataValueFormGroup(); + + const metadataValue = service.getMetadataValue(formGroup) as any; + + expect(metadataValue).toMatchObject({}); + }); + + it('should return IMetadataValue', () => { + const formGroup = service.createMetadataValueFormGroup(sampleWithRequiredData); + + const metadataValue = service.getMetadataValue(formGroup) as any; + + expect(metadataValue).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IMetadataValue should not enable id FormControl', () => { + const formGroup = service.createMetadataValueFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewMetadataValue should disable id FormControl', () => { + const formGroup = service.createMetadataValueFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-value/update/metadata-value-form.service.ts b/src/main/webapp/app/entities/metadata-value/update/metadata-value-form.service.ts new file mode 100644 index 0000000..d0cb0c9 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/update/metadata-value-form.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IMetadataValue, NewMetadataValue } from '../metadata-value.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IMetadataValue for edit and NewMetadataValueFormGroupInput for create. + */ +type MetadataValueFormGroupInput = IMetadataValue | PartialWithRequiredKeyOf; + +type MetadataValueFormDefaults = Pick; + +type MetadataValueFormGroupContent = { + id: FormControl; + metadataPropertyCode: FormControl; + targetDomainKey: FormControl; + targetDomainId: FormControl; + value: FormControl; + version: FormControl; +}; + +export type MetadataValueFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class MetadataValueFormService { + createMetadataValueFormGroup(metadataValue: MetadataValueFormGroupInput = { id: null }): MetadataValueFormGroup { + const metadataValueRawValue = { + ...this.getFormDefaults(), + ...metadataValue, + }; + return new FormGroup({ + id: new FormControl( + { value: metadataValueRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + metadataPropertyCode: new FormControl(metadataValueRawValue.metadataPropertyCode, { + validators: [Validators.required], + }), + targetDomainKey: new FormControl(metadataValueRawValue.targetDomainKey, { + validators: [Validators.required], + }), + targetDomainId: new FormControl(metadataValueRawValue.targetDomainId, { + validators: [Validators.required], + }), + value: new FormControl(metadataValueRawValue.value), + version: new FormControl(metadataValueRawValue.version), + }); + } + + getMetadataValue(form: MetadataValueFormGroup): IMetadataValue | NewMetadataValue { + return form.getRawValue() as IMetadataValue | NewMetadataValue; + } + + resetForm(form: MetadataValueFormGroup, metadataValue: MetadataValueFormGroupInput): void { + const metadataValueRawValue = { ...this.getFormDefaults(), ...metadataValue }; + form.reset( + { + ...metadataValueRawValue, + id: { value: metadataValueRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): MetadataValueFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.html b/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.html new file mode 100644 index 0000000..e664976 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.html @@ -0,0 +1,150 @@ +
+
+
+

+ Criar ou editar Metadata Value +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if ( + editForm.get('metadataPropertyCode')!.invalid && + (editForm.get('metadataPropertyCode')!.dirty || editForm.get('metadataPropertyCode')!.touched) + ) { +
+ @if (editForm.get('metadataPropertyCode')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if ( + editForm.get('targetDomainKey')!.invalid && (editForm.get('targetDomainKey')!.dirty || editForm.get('targetDomainKey')!.touched) + ) { +
+ @if (editForm.get('targetDomainKey')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if ( + editForm.get('targetDomainId')!.invalid && (editForm.get('targetDomainId')!.dirty || editForm.get('targetDomainId')!.touched) + ) { +
+ @if (editForm.get('targetDomainId')?.errors?.required) { + O campo é obrigatório. + } + Este campo é do tipo número. +
+ } +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.spec.ts b/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.spec.ts new file mode 100644 index 0000000..06f8d42 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { MetadataValueService } from '../service/metadata-value.service'; +import { IMetadataValue } from '../metadata-value.model'; +import { MetadataValueFormService } from './metadata-value-form.service'; + +import { MetadataValueUpdateComponent } from './metadata-value-update.component'; + +describe('MetadataValue Management Update Component', () => { + let comp: MetadataValueUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let metadataValueFormService: MetadataValueFormService; + let metadataValueService: MetadataValueService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MetadataValueUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(MetadataValueUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(MetadataValueUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + metadataValueFormService = TestBed.inject(MetadataValueFormService); + metadataValueService = TestBed.inject(MetadataValueService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should update editForm', () => { + const metadataValue: IMetadataValue = { id: 456 }; + + activatedRoute.data = of({ metadataValue }); + comp.ngOnInit(); + + expect(comp.metadataValue).toEqual(metadataValue); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const metadataValue = { id: 123 }; + jest.spyOn(metadataValueFormService, 'getMetadataValue').mockReturnValue(metadataValue); + jest.spyOn(metadataValueService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ metadataValue }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: metadataValue })); + saveSubject.complete(); + + // THEN + expect(metadataValueFormService.getMetadataValue).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(metadataValueService.update).toHaveBeenCalledWith(expect.objectContaining(metadataValue)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const metadataValue = { id: 123 }; + jest.spyOn(metadataValueFormService, 'getMetadataValue').mockReturnValue({ id: null }); + jest.spyOn(metadataValueService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ metadataValue: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: metadataValue })); + saveSubject.complete(); + + // THEN + expect(metadataValueFormService.getMetadataValue).toHaveBeenCalled(); + expect(metadataValueService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const metadataValue = { id: 123 }; + jest.spyOn(metadataValueService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ metadataValue }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(metadataValueService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.ts b/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.ts new file mode 100644 index 0000000..d1f2808 --- /dev/null +++ b/src/main/webapp/app/entities/metadata-value/update/metadata-value-update.component.ts @@ -0,0 +1,77 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IMetadataValue } from '../metadata-value.model'; +import { MetadataValueService } from '../service/metadata-value.service'; +import { MetadataValueFormService, MetadataValueFormGroup } from './metadata-value-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-metadata-value-update', + templateUrl: './metadata-value-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class MetadataValueUpdateComponent implements OnInit { + isSaving = false; + metadataValue: IMetadataValue | null = null; + + protected metadataValueService = inject(MetadataValueService); + protected metadataValueFormService = inject(MetadataValueFormService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: MetadataValueFormGroup = this.metadataValueFormService.createMetadataValueFormGroup(); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ metadataValue }) => { + this.metadataValue = metadataValue; + if (metadataValue) { + this.updateForm(metadataValue); + } + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const metadataValue = this.metadataValueFormService.getMetadataValue(this.editForm); + if (metadataValue.id !== null) { + this.subscribeToSaveResponse(this.metadataValueService.update(metadataValue)); + } else { + this.subscribeToSaveResponse(this.metadataValueService.create(metadataValue)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(metadataValue: IMetadataValue): void { + this.metadataValue = metadataValue; + this.metadataValueFormService.resetForm(this.editForm, metadataValue); + } +} diff --git a/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.html b/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.html new file mode 100644 index 0000000..8044a6e --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (organizationType) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.spec.ts new file mode 100644 index 0000000..677779a --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { OrganizationTypeService } from '../service/organization-type.service'; + +import { OrganizationTypeDeleteDialogComponent } from './organization-type-delete-dialog.component'; + +describe('OrganizationType Management Delete Component', () => { + let comp: OrganizationTypeDeleteDialogComponent; + let fixture: ComponentFixture; + let service: OrganizationTypeService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, OrganizationTypeDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(OrganizationTypeDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(OrganizationTypeDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(OrganizationTypeService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.ts b/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.ts new file mode 100644 index 0000000..c88454d --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/delete/organization-type-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IOrganizationType } from '../organization-type.model'; +import { OrganizationTypeService } from '../service/organization-type.service'; + +@Component({ + standalone: true, + templateUrl: './organization-type-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class OrganizationTypeDeleteDialogComponent { + organizationType?: IOrganizationType; + + protected organizationTypeService = inject(OrganizationTypeService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.organizationTypeService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.html b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.html new file mode 100644 index 0000000..924b475 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.html @@ -0,0 +1,113 @@ + +
+
+ @if (organizationType()) { +
+

+ Organization Type +

+ +
+ + + + + +
+
+ Code +
+
+ {{ organizationType()!.code }} +
+
+ Name +
+
+ {{ organizationType()!.name }} +
+
+ Description +
+
+ {{ organizationType()!.description }} +
+
+ Nature +
+
+ {{ + { null: '', ORGANIZATION: 'ORGANIZATION', PERSON: 'PERSON', FACILITY: 'FACILITY', LEVEL: 'LEVEL' }[ + organizationType()!.nature ?? 'null' + ] + }} +
+
+ Icon +
+
+ @if (organizationType()!.icon) { +
+ + organizationType + + {{ organizationType()!.iconContentType }}, {{ byteSize(organizationType()!.icon ?? '') }} +
+ } +
+
Metadata Properties
+
+ @for (metadataProperties of organizationType()!.metadataProperties; track $index; let last = $last) { + + {{ metadataProperties?.name }}{{ last ? '' : ', ' }} + + } +
+ + + +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.html.old b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.html.old new file mode 100644 index 0000000..a41a1bb --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.html.old @@ -0,0 +1,108 @@ +
+
+ @if (organizationType()) { +
+

+ Organization Type +

+ +
+ + + + + +
+
Código
+
+ {{ organizationType()!.id }} +
+
+ Code +
+
+ {{ organizationType()!.code }} +
+
+ Name +
+
+ {{ organizationType()!.name }} +
+
+ Description +
+
+ {{ organizationType()!.description }} +
+
+ Nature +
+
+ {{ + { null: '', ORGANIZATION: 'ORGANIZATION', PERSON: 'PERSON', FACILITY: 'FACILITY', LEVEL: 'LEVEL' }[ + organizationType()!.nature ?? 'null' + ] + }} +
+
+ Icon +
+
+ @if (organizationType()!.icon) { +
+ + organizationType + + {{ organizationType()!.iconContentType }}, {{ byteSize(organizationType()!.icon ?? '') }} +
+ } +
+
+ Version +
+
+ {{ organizationType()!.version }} +
+
Metadata Properties
+
+ @for (metadataProperties of organizationType()!.metadataProperties; track $index; let last = $last) { + + {{ metadataProperties?.name }}{{ last ? '' : ', ' }} + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.spec.ts b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.spec.ts new file mode 100644 index 0000000..5a8ecc5 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.spec.ts @@ -0,0 +1,93 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { DataUtils } from 'app/core/util/data-util.service'; + +import { OrganizationTypeDetailComponent } from './organization-type-detail.component'; + +describe('OrganizationType Management Detail Component', () => { + let comp: OrganizationTypeDetailComponent; + let fixture: ComponentFixture; + let dataUtils: DataUtils; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OrganizationTypeDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: OrganizationTypeDetailComponent, + resolve: { organizationType: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(OrganizationTypeDetailComponent, '') + .compileComponents(); + dataUtils = TestBed.inject(DataUtils); + jest.spyOn(window, 'open').mockImplementation(() => null); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OrganizationTypeDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load organizationType on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', OrganizationTypeDetailComponent); + + // THEN + expect(instance.organizationType()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); + + describe('byteSize', () => { + it('Should call byteSize from DataUtils', () => { + // GIVEN + jest.spyOn(dataUtils, 'byteSize'); + const fakeBase64 = 'fake base64'; + + // WHEN + comp.byteSize(fakeBase64); + + // THEN + expect(dataUtils.byteSize).toHaveBeenCalledWith(fakeBase64); + }); + }); + + describe('openFile', () => { + it('Should call openFile from DataUtils', () => { + const newWindow = { ...window }; + newWindow.document.write = jest.fn(); + window.open = jest.fn(() => newWindow); + window.onload = jest.fn(() => newWindow) as any; + window.URL.createObjectURL = jest.fn() as any; + // GIVEN + jest.spyOn(dataUtils, 'openFile'); + const fakeContentType = 'fake content type'; + const fakeBase64 = 'fake base64'; + + // WHEN + comp.openFile(fakeBase64, fakeContentType); + + // THEN + expect(dataUtils.openFile).toHaveBeenCalledWith(fakeBase64, fakeContentType); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.ts b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.ts new file mode 100644 index 0000000..fd29f32 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/detail/organization-type-detail.component.ts @@ -0,0 +1,31 @@ +import { Component, inject, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { IOrganizationType } from '../organization-type.model'; + +@Component({ + standalone: true, + selector: 'jhi-organization-type-detail', + templateUrl: './organization-type-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class OrganizationTypeDetailComponent { + organizationType = input(null); + + protected dataUtils = inject(DataUtils); + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/organization-type/list/organization-type.component.html b/src/main/webapp/app/entities/organization-type/list/organization-type.component.html new file mode 100644 index 0000000..dfcf1dd --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/list/organization-type.component.html @@ -0,0 +1,111 @@ + +
+

+ Organization Types + +
+ + + +
+

+ + + + + + @if (organizationTypes?.length === 0) { +
+ Nenhum Organization Types encontrado +
+ } + + @if (organizationTypes && organizationTypes.length > 0) { +
+ + + + + + + + + + + @for (organizationType of organizationTypes; track trackId) { + + + + + + + + } + +
+
+ Icon + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+ @if (organizationType.icon) { + + organizationType + } + {{ organizationType.name }}{{ organizationType.description }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/organization-type/list/organization-type.component.spec.ts b/src/main/webapp/app/entities/organization-type/list/organization-type.component.spec.ts new file mode 100644 index 0000000..59ca5e2 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/list/organization-type.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../organization-type.test-samples'; +import { OrganizationTypeService } from '../service/organization-type.service'; + +import { OrganizationTypeComponent } from './organization-type.component'; +import SpyInstance = jest.SpyInstance; + +describe('OrganizationType Management Component', () => { + let comp: OrganizationTypeComponent; + let fixture: ComponentFixture; + let service: OrganizationTypeService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, OrganizationTypeComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(OrganizationTypeComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationTypeComponent); + comp = fixture.componentInstance; + service = TestBed.inject(OrganizationTypeService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.organizationTypes?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to organizationTypeService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getOrganizationTypeIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getOrganizationTypeIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/organization-type/list/organization-type.component.ts b/src/main/webapp/app/entities/organization-type/list/organization-type.component.ts new file mode 100644 index 0000000..6d3b022 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/list/organization-type.component.ts @@ -0,0 +1,132 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { IOrganizationType } from '../organization-type.model'; +import { EntityArrayResponseType, OrganizationTypeService } from '../service/organization-type.service'; +import { OrganizationTypeDeleteDialogComponent } from '../delete/organization-type-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-organization-type', + templateUrl: './organization-type.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class OrganizationTypeComponent implements OnInit { + subscription: Subscription | null = null; + organizationTypes?: IOrganizationType[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected organizationTypeService = inject(OrganizationTypeService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected dataUtils = inject(DataUtils); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IOrganizationType): number => this.organizationTypeService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.organizationTypes || this.organizationTypes.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + return this.dataUtils.openFile(base64String, contentType); + } + + delete(organizationType: IOrganizationType): void { + const modalRef = this.modalService.open(OrganizationTypeDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.organizationType = organizationType; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.organizationTypes = this.refineData(dataFromBody); + } + + protected refineData(data: IOrganizationType[]): IOrganizationType[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IOrganizationType[] | null): IOrganizationType[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.organizationTypeService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/organization-type/organization-type.model.ts b/src/main/webapp/app/entities/organization-type/organization-type.model.ts new file mode 100644 index 0000000..649734a --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/organization-type.model.ts @@ -0,0 +1,16 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IMetadataProperty } from 'app/entities/metadata-property/metadata-property.model'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; + +export interface IOrganizationType extends IResilientEntity { + code?: string | null; + name?: string | null; + description?: string | null; + nature?: keyof typeof OrganizationNature | null; + icon?: string | null; + iconContentType?: string | null; + metadataProperties?: Pick[] | null; +} + +export type NewOrganizationType = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/organization-type/organization-type.routes.ts b/src/main/webapp/app/entities/organization-type/organization-type.routes.ts new file mode 100644 index 0000000..5486118 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/organization-type.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { OrganizationTypeComponent } from './list/organization-type.component'; +import { OrganizationTypeDetailComponent } from './detail/organization-type-detail.component'; +import { OrganizationTypeUpdateComponent } from './update/organization-type-update.component'; +import OrganizationTypeResolve from './route/organization-type-routing-resolve.service'; +import { Resources } from 'app/security/resources.model'; +import { OrganizationTypeService } from './service/organization-type.service'; +import { SecurityAction } from 'app/security/security-action.model'; + +const organizationTypeRoute: Routes = [ + { + path: '', + component: OrganizationTypeComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: OrganizationTypeService, + resource: Resources.ORGANIZATION_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: OrganizationTypeDetailComponent, + resolve: { + organizationType: OrganizationTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: OrganizationTypeService, + resource: Resources.ORGANIZATION_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: OrganizationTypeUpdateComponent, + resolve: { + organizationType: OrganizationTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: OrganizationTypeService, + resource: Resources.ORGANIZATION_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: OrganizationTypeUpdateComponent, + resolve: { + organizationType: OrganizationTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: OrganizationTypeService, + resource: Resources.ORGANIZATION_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default organizationTypeRoute; diff --git a/src/main/webapp/app/entities/organization-type/organization-type.test-samples.ts b/src/main/webapp/app/entities/organization-type/organization-type.test-samples.ts new file mode 100644 index 0000000..b2afe0f --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/organization-type.test-samples.ts @@ -0,0 +1,42 @@ +import { IOrganizationType, NewOrganizationType } from './organization-type.model'; + +export const sampleWithRequiredData: IOrganizationType = { + id: 10209, + code: 'as different', + name: 'through apropos yawn', + description: 'on who excluding', + nature: 'LEVEL', +}; + +export const sampleWithPartialData: IOrganizationType = { + id: 7125, + code: 'yearningly', + name: 'mishandle whoa neatly', + description: 'preen spring waitress', + nature: 'FACILITY', + version: 7328, +}; + +export const sampleWithFullData: IOrganizationType = { + id: 272, + code: 'pummel within pepper', + name: 'excluding', + description: 'ecology whoever', + nature: 'ORGANIZATION', + icon: '../fake-data/blob/hipster.png', + iconContentType: 'unknown', + version: 4731, +}; + +export const sampleWithNewData: NewOrganizationType = { + code: 'sudden magnificent knowingly', + name: 'pajamas enquiry', + description: 'lest', + nature: 'FACILITY', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/organization-type/route/organization-type-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/organization-type/route/organization-type-routing-resolve.service.spec.ts new file mode 100644 index 0000000..4be3840 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/route/organization-type-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IOrganizationType } from '../organization-type.model'; +import { OrganizationTypeService } from '../service/organization-type.service'; + +import organizationTypeResolve from './organization-type-routing-resolve.service'; + +describe('OrganizationType routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: OrganizationTypeService; + let resultOrganizationType: IOrganizationType | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(OrganizationTypeService); + resultOrganizationType = undefined; + }); + + describe('resolve', () => { + it('should return IOrganizationType returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + organizationTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOrganizationType = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultOrganizationType).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + organizationTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOrganizationType = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultOrganizationType).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + organizationTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOrganizationType = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultOrganizationType).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization-type/route/organization-type-routing-resolve.service.ts b/src/main/webapp/app/entities/organization-type/route/organization-type-routing-resolve.service.ts new file mode 100644 index 0000000..95290e2 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/route/organization-type-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IOrganizationType } from '../organization-type.model'; +import { OrganizationTypeService } from '../service/organization-type.service'; + +const organizationTypeResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(OrganizationTypeService) + .find(id) + .pipe( + mergeMap((organizationType: HttpResponse) => { + if (organizationType.body) { + return of(organizationType.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default organizationTypeResolve; diff --git a/src/main/webapp/app/entities/organization-type/service/organization-type.service.spec.ts b/src/main/webapp/app/entities/organization-type/service/organization-type.service.spec.ts new file mode 100644 index 0000000..6b9d843 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/service/organization-type.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IOrganizationType } from '../organization-type.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../organization-type.test-samples'; + +import { OrganizationTypeService } from './organization-type.service'; + +const requireRestSample: IOrganizationType = { + ...sampleWithRequiredData, +}; + +describe('OrganizationType Service', () => { + let service: OrganizationTypeService; + let httpMock: HttpTestingController; + let expectedResult: IOrganizationType | IOrganizationType[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(OrganizationTypeService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a OrganizationType', () => { + const organizationType = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(organizationType).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a OrganizationType', () => { + const organizationType = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(organizationType).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a OrganizationType', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of OrganizationType', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a OrganizationType', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addOrganizationTypeToCollectionIfMissing', () => { + it('should add a OrganizationType to an empty array', () => { + const organizationType: IOrganizationType = sampleWithRequiredData; + expectedResult = service.addOrganizationTypeToCollectionIfMissing([], organizationType); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(organizationType); + }); + + it('should not add a OrganizationType to an array that contains it', () => { + const organizationType: IOrganizationType = sampleWithRequiredData; + const organizationTypeCollection: IOrganizationType[] = [ + { + ...organizationType, + }, + sampleWithPartialData, + ]; + expectedResult = service.addOrganizationTypeToCollectionIfMissing(organizationTypeCollection, organizationType); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a OrganizationType to an array that doesn't contain it", () => { + const organizationType: IOrganizationType = sampleWithRequiredData; + const organizationTypeCollection: IOrganizationType[] = [sampleWithPartialData]; + expectedResult = service.addOrganizationTypeToCollectionIfMissing(organizationTypeCollection, organizationType); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(organizationType); + }); + + it('should add only unique OrganizationType to an array', () => { + const organizationTypeArray: IOrganizationType[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const organizationTypeCollection: IOrganizationType[] = [sampleWithRequiredData]; + expectedResult = service.addOrganizationTypeToCollectionIfMissing(organizationTypeCollection, ...organizationTypeArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const organizationType: IOrganizationType = sampleWithRequiredData; + const organizationType2: IOrganizationType = sampleWithPartialData; + expectedResult = service.addOrganizationTypeToCollectionIfMissing([], organizationType, organizationType2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(organizationType); + expect(expectedResult).toContain(organizationType2); + }); + + it('should accept null and undefined values', () => { + const organizationType: IOrganizationType = sampleWithRequiredData; + expectedResult = service.addOrganizationTypeToCollectionIfMissing([], null, organizationType, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(organizationType); + }); + + it('should return initial array if no OrganizationType is added', () => { + const organizationTypeCollection: IOrganizationType[] = [sampleWithRequiredData]; + expectedResult = service.addOrganizationTypeToCollectionIfMissing(organizationTypeCollection, undefined, null); + expect(expectedResult).toEqual(organizationTypeCollection); + }); + }); + + describe('compareOrganizationType', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareOrganizationType(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareOrganizationType(entity1, entity2); + const compareResult2 = service.compareOrganizationType(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareOrganizationType(entity1, entity2); + const compareResult2 = service.compareOrganizationType(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareOrganizationType(entity1, entity2); + const compareResult2 = service.compareOrganizationType(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/organization-type/service/organization-type.service.ts b/src/main/webapp/app/entities/organization-type/service/organization-type.service.ts new file mode 100644 index 0000000..ea087b2 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/service/organization-type.service.ts @@ -0,0 +1,25 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IOrganizationType, NewOrganizationType } from '../organization-type.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class OrganizationTypeService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/organization-types'); + getResourceUrl(): string { return this.resourceUrl; } + + queryByNature(nature: string): Observable { + return this.http.get(`${this.resourceUrl}/nature/${nature}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/organization-type/update/organization-type-form.service.spec.ts b/src/main/webapp/app/entities/organization-type/update/organization-type-form.service.spec.ts new file mode 100644 index 0000000..e96c00e --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/update/organization-type-form.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../organization-type.test-samples'; + +import { OrganizationTypeFormService } from './organization-type-form.service'; + +describe('OrganizationType Form Service', () => { + let service: OrganizationTypeFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OrganizationTypeFormService); + }); + + describe('Service methods', () => { + describe('createOrganizationTypeFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createOrganizationTypeFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + nature: expect.any(Object), + icon: expect.any(Object), + version: expect.any(Object), + metadataProperties: expect.any(Object), + }), + ); + }); + + it('passing IOrganizationType should create a new form with FormGroup', () => { + const formGroup = service.createOrganizationTypeFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + nature: expect.any(Object), + icon: expect.any(Object), + version: expect.any(Object), + metadataProperties: expect.any(Object), + }), + ); + }); + }); + + describe('getOrganizationType', () => { + it('should return NewOrganizationType for default OrganizationType initial value', () => { + const formGroup = service.createOrganizationTypeFormGroup(sampleWithNewData); + + const organizationType = service.getOrganizationType(formGroup) as any; + + expect(organizationType).toMatchObject(sampleWithNewData); + }); + + it('should return NewOrganizationType for empty OrganizationType initial value', () => { + const formGroup = service.createOrganizationTypeFormGroup(); + + const organizationType = service.getOrganizationType(formGroup) as any; + + expect(organizationType).toMatchObject({}); + }); + + it('should return IOrganizationType', () => { + const formGroup = service.createOrganizationTypeFormGroup(sampleWithRequiredData); + + const organizationType = service.getOrganizationType(formGroup) as any; + + expect(organizationType).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IOrganizationType should not enable id FormControl', () => { + const formGroup = service.createOrganizationTypeFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewOrganizationType should disable id FormControl', () => { + const formGroup = service.createOrganizationTypeFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization-type/update/organization-type-form.service.ts b/src/main/webapp/app/entities/organization-type/update/organization-type-form.service.ts new file mode 100644 index 0000000..d0c7b98 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/update/organization-type-form.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IOrganizationType, NewOrganizationType } from '../organization-type.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IOrganizationType for edit and NewOrganizationTypeFormGroupInput for create. + */ +type OrganizationTypeFormGroupInput = IOrganizationType | PartialWithRequiredKeyOf; + +type OrganizationTypeFormDefaults = Pick; + +type OrganizationTypeFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + description: FormControl; + nature: FormControl; + icon: FormControl; + iconContentType: FormControl; + version: FormControl; + metadataProperties: FormControl; +}; + +export type OrganizationTypeFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class OrganizationTypeFormService { + createOrganizationTypeFormGroup(organizationType: OrganizationTypeFormGroupInput = { id: null }): OrganizationTypeFormGroup { + const organizationTypeRawValue = { + ...this.getFormDefaults(), + ...organizationType, + }; + return new FormGroup({ + id: new FormControl( + { value: organizationTypeRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(organizationTypeRawValue.code, { + validators: [Validators.required, Validators.minLength(1)], + }), + name: new FormControl(organizationTypeRawValue.name, { + validators: [Validators.required, Validators.minLength(3)], + }), + description: new FormControl(organizationTypeRawValue.description, { + validators: [Validators.required, Validators.minLength(3)], + }), + nature: new FormControl(organizationTypeRawValue.nature, { + validators: [Validators.required], + }), + icon: new FormControl(organizationTypeRawValue.icon), + iconContentType: new FormControl(organizationTypeRawValue.iconContentType), + version: new FormControl(organizationTypeRawValue.version), + metadataProperties: new FormControl(organizationTypeRawValue.metadataProperties ?? []), + }); + } + + getOrganizationType(form: OrganizationTypeFormGroup): IOrganizationType | NewOrganizationType { + return form.getRawValue() as IOrganizationType | NewOrganizationType; + } + + resetForm(form: OrganizationTypeFormGroup, organizationType: OrganizationTypeFormGroupInput): void { + const organizationTypeRawValue = { ...this.getFormDefaults(), ...organizationType }; + form.reset( + { + ...organizationTypeRawValue, + id: { value: organizationTypeRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): OrganizationTypeFormDefaults { + return { + id: null, + metadataProperties: [], + }; + } +} diff --git a/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.html b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.html new file mode 100644 index 0000000..090907b --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.html @@ -0,0 +1,215 @@ + +
+
+
+

+ Criar ou editar Organization Type +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('name')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('description')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('nature')!.invalid && (editForm.get('nature')!.dirty || editForm.get('nature')!.touched)) { +
+ @if (editForm.get('nature')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ +
+ @if (editForm.get('icon')!.value) { + organizationType + } + @if (editForm.get('icon')!.value) { +
+ {{ editForm.get('iconContentType')!.value }}, {{ byteSize(editForm.get('icon')!.value!) }} + +
+ } + +
+ + +
+
+ + +
+ + + +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.html.old b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.html.old new file mode 100644 index 0000000..f271559 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.html.old @@ -0,0 +1,208 @@ +
+
+
+

+ Criar ou editar Organization Type +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('name')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+ +
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('description')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+ +
+ + + @if (editForm.get('nature')!.invalid && (editForm.get('nature')!.dirty || editForm.get('nature')!.touched)) { +
+ @if (editForm.get('nature')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ +
+ @if (editForm.get('icon')!.value) { + organizationType + } + @if (editForm.get('icon')!.value) { +
+ {{ editForm.get('iconContentType')!.value }}, {{ byteSize(editForm.get('icon')!.value!) }} + +
+ } + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.spec.ts b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.spec.ts new file mode 100644 index 0000000..4dc9ba0 --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.spec.ts @@ -0,0 +1,164 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IMetadataProperty } from 'app/entities/metadata-property/metadata-property.model'; +import { MetadataPropertyService } from 'app/entities/metadata-property/service/metadata-property.service'; +import { OrganizationTypeService } from '../service/organization-type.service'; +import { IOrganizationType } from '../organization-type.model'; +import { OrganizationTypeFormService } from './organization-type-form.service'; + +import { OrganizationTypeUpdateComponent } from './organization-type-update.component'; + +describe('OrganizationType Management Update Component', () => { + let comp: OrganizationTypeUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let organizationTypeFormService: OrganizationTypeFormService; + let organizationTypeService: OrganizationTypeService; + let metadataPropertyService: MetadataPropertyService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, OrganizationTypeUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(OrganizationTypeUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationTypeUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + organizationTypeFormService = TestBed.inject(OrganizationTypeFormService); + organizationTypeService = TestBed.inject(OrganizationTypeService); + metadataPropertyService = TestBed.inject(MetadataPropertyService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call MetadataProperty query and add missing value', () => { + const organizationType: IOrganizationType = { id: 456 }; + const metadataProperties: IMetadataProperty[] = [{ id: 4515 }]; + organizationType.metadataProperties = metadataProperties; + + const metadataPropertyCollection: IMetadataProperty[] = [{ id: 16236 }]; + jest.spyOn(metadataPropertyService, 'query').mockReturnValue(of(new HttpResponse({ body: metadataPropertyCollection }))); + const additionalMetadataProperties = [...metadataProperties]; + const expectedCollection: IMetadataProperty[] = [...additionalMetadataProperties, ...metadataPropertyCollection]; + jest.spyOn(metadataPropertyService, 'addMetadataPropertyToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ organizationType }); + comp.ngOnInit(); + + expect(metadataPropertyService.query).toHaveBeenCalled(); + expect(metadataPropertyService.addMetadataPropertyToCollectionIfMissing).toHaveBeenCalledWith( + metadataPropertyCollection, + ...additionalMetadataProperties.map(expect.objectContaining), + ); + expect(comp.metadataPropertiesSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const organizationType: IOrganizationType = { id: 456 }; + const metadataProperties: IMetadataProperty = { id: 29219 }; + organizationType.metadataProperties = [metadataProperties]; + + activatedRoute.data = of({ organizationType }); + comp.ngOnInit(); + + expect(comp.metadataPropertiesSharedCollection).toContain(metadataProperties); + expect(comp.organizationType).toEqual(organizationType); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const organizationType = { id: 123 }; + jest.spyOn(organizationTypeFormService, 'getOrganizationType').mockReturnValue(organizationType); + jest.spyOn(organizationTypeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ organizationType }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: organizationType })); + saveSubject.complete(); + + // THEN + expect(organizationTypeFormService.getOrganizationType).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(organizationTypeService.update).toHaveBeenCalledWith(expect.objectContaining(organizationType)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const organizationType = { id: 123 }; + jest.spyOn(organizationTypeFormService, 'getOrganizationType').mockReturnValue({ id: null }); + jest.spyOn(organizationTypeService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ organizationType: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: organizationType })); + saveSubject.complete(); + + // THEN + expect(organizationTypeFormService.getOrganizationType).toHaveBeenCalled(); + expect(organizationTypeService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const organizationType = { id: 123 }; + jest.spyOn(organizationTypeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ organizationType }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(organizationTypeService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareMetadataProperty', () => { + it('Should forward to metadataPropertyService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(metadataPropertyService, 'compareMetadataProperty'); + comp.compareMetadataProperty(entity, entity2); + expect(metadataPropertyService.compareMetadataProperty).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.ts b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.ts new file mode 100644 index 0000000..7bffa9f --- /dev/null +++ b/src/main/webapp/app/entities/organization-type/update/organization-type-update.component.ts @@ -0,0 +1,140 @@ +import { Component, inject, OnInit, ElementRef } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { AlertError } from 'app/shared/alert/alert-error.model'; +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; +import { DataUtils, FileLoadError } from 'app/core/util/data-util.service'; +import { IMetadataProperty } from 'app/entities/metadata-property/metadata-property.model'; +import { MetadataPropertyService } from 'app/entities/metadata-property/service/metadata-property.service'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; +import { OrganizationTypeService } from '../service/organization-type.service'; +import { IOrganizationType } from '../organization-type.model'; +import { OrganizationTypeFormService, OrganizationTypeFormGroup } from './organization-type-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-organization-type-update', + templateUrl: './organization-type-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class OrganizationTypeUpdateComponent implements OnInit { + isSaving = false; + organizationType: IOrganizationType | null = null; + organizationNatureValues = Object.keys(OrganizationNature); + + metadataPropertiesSharedCollection: IMetadataProperty[] = []; + + protected dataUtils = inject(DataUtils); + protected eventManager = inject(EventManager); + protected organizationTypeService = inject(OrganizationTypeService); + protected organizationTypeFormService = inject(OrganizationTypeFormService); + protected metadataPropertyService = inject(MetadataPropertyService); + protected elementRef = inject(ElementRef); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: OrganizationTypeFormGroup = this.organizationTypeFormService.createOrganizationTypeFormGroup(); + + compareMetadataProperty = (o1: IMetadataProperty | null, o2: IMetadataProperty | null): boolean => + this.metadataPropertyService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ organizationType }) => { + this.organizationType = organizationType; + if (organizationType) { + this.updateForm(organizationType); + } + + this.loadRelationshipsOptions(); + }); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + setFileData(event: Event, field: string, isImage: boolean): void { + this.dataUtils.loadFileToForm(event, this.editForm, field, isImage).subscribe({ + error: (err: FileLoadError) => + this.eventManager.broadcast(new EventWithContent('resilientApp.error', { ...err, key: 'error.file.' + err.key })), + }); + } + + clearInputImage(field: string, fieldContentType: string, idInput: string): void { + this.editForm.patchValue({ + [field]: null, + [fieldContentType]: null, + }); + if (idInput && this.elementRef.nativeElement.querySelector('#' + idInput)) { + this.elementRef.nativeElement.querySelector('#' + idInput).value = null; + } + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const organizationType = this.organizationTypeFormService.getOrganizationType(this.editForm); + if (organizationType.id !== null) { + this.subscribeToSaveResponse(this.organizationTypeService.update(organizationType)); + } else { + this.subscribeToSaveResponse(this.organizationTypeService.create(organizationType)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(organizationType: IOrganizationType): void { + this.organizationType = organizationType; + this.organizationTypeFormService.resetForm(this.editForm, organizationType); + + this.metadataPropertiesSharedCollection = this.metadataPropertyService.addEntityToCollectionIfMissing( + this.metadataPropertiesSharedCollection, + ...(organizationType.metadataProperties ?? []), + ); + } + + protected loadRelationshipsOptions(): void { + this.metadataPropertyService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((metadataProperties: IMetadataProperty[]) => + this.metadataPropertyService.addEntityToCollectionIfMissing( + metadataProperties, + ...(this.organizationType?.metadataProperties ?? []), + ), + ), + ) + .subscribe((metadataProperties: IMetadataProperty[]) => (this.metadataPropertiesSharedCollection = metadataProperties)); + } +} diff --git a/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.html b/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.html new file mode 100644 index 0000000..5e1231f --- /dev/null +++ b/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (organization) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.spec.ts new file mode 100644 index 0000000..04fcbb7 --- /dev/null +++ b/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { OrganizationService } from '../service/organization.service'; + +import { OrganizationDeleteDialogComponent } from './organization-delete-dialog.component'; + +describe('Organization Management Delete Component', () => { + let comp: OrganizationDeleteDialogComponent; + let fixture: ComponentFixture; + let service: OrganizationService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, OrganizationDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(OrganizationDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(OrganizationDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(OrganizationService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.ts b/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.ts new file mode 100644 index 0000000..dd88135 --- /dev/null +++ b/src/main/webapp/app/entities/organization/delete/organization-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IOrganization } from '../organization.model'; +import { OrganizationService } from '../service/organization.service'; + +@Component({ + standalone: true, + templateUrl: './organization-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class OrganizationDeleteDialogComponent { + organization?: IOrganization; + + protected organizationService = inject(OrganizationService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.organizationService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/organization/detail/organization-detail.component.html b/src/main/webapp/app/entities/organization/detail/organization-detail.component.html new file mode 100644 index 0000000..24fec28 --- /dev/null +++ b/src/main/webapp/app/entities/organization/detail/organization-detail.component.html @@ -0,0 +1,104 @@ + +
+
+ @if (organization()) { +
+

Organization

+ +
+ + + + + +
+
Parent
+
+ @if (organization()!.parent) { + + } +
+
Organization Type
+
+ @if (organization()!.organizationType) { + + } +
+
+ Code +
+
+ {{ organization()!.code }} +
+
+ Name +
+
+ {{ organization()!.name }} +
+
Image
+
+ @if (organization()!.image) { +
+ + organization + + {{ organization()!.imageContentType }}, {{ byteSize(organization()!.image ?? '') }} +
+ } +
+ @if (organization()!.organizationType?.nature === organizationNature.ORGANIZATION) { +
Input Inventory
+
+ {{ organization()!.inputInventory }} +
+
Output Inventory
+
+ {{ organization()!.outputInventory }} +
+ } + @if (organization()!.organizationType?.nature === organizationNature.PERSON) { +
User
+
+ {{ organization()!.user?.login }} +
+ } + + + +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/organization/detail/organization-detail.component.html.old b/src/main/webapp/app/entities/organization/detail/organization-detail.component.html.old new file mode 100644 index 0000000..d487bdf --- /dev/null +++ b/src/main/webapp/app/entities/organization/detail/organization-detail.component.html.old @@ -0,0 +1,95 @@ +
+
+ @if (organization()) { +
+

Organization

+ +
+ + + + + +
+
Código
+
+ {{ organization()!.id }} +
+
+ Code +
+
+ {{ organization()!.code }} +
+
+ Name +
+
+ {{ organization()!.name }} +
+
Image
+
+ @if (organization()!.image) { +
+ + organization + + {{ organization()!.imageContentType }}, {{ byteSize(organization()!.image ?? '') }} +
+ } +
+
Input Inventory
+
+ {{ organization()!.inputInventory }} +
+
Output Inventory
+
+ {{ organization()!.outputInventory }} +
+
+ Version +
+
+ {{ organization()!.version }} +
+
User
+
+ {{ organization()!.user?.login }} +
+
Parent
+
+ @if (organization()!.parent) { + + } +
+
Organization Type
+
+ @if (organization()!.organizationType) { + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/organization/detail/organization-detail.component.spec.ts b/src/main/webapp/app/entities/organization/detail/organization-detail.component.spec.ts new file mode 100644 index 0000000..92def5d --- /dev/null +++ b/src/main/webapp/app/entities/organization/detail/organization-detail.component.spec.ts @@ -0,0 +1,93 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { DataUtils } from 'app/core/util/data-util.service'; + +import { OrganizationDetailComponent } from './organization-detail.component'; + +describe('Organization Management Detail Component', () => { + let comp: OrganizationDetailComponent; + let fixture: ComponentFixture; + let dataUtils: DataUtils; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OrganizationDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: OrganizationDetailComponent, + resolve: { organization: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(OrganizationDetailComponent, '') + .compileComponents(); + dataUtils = TestBed.inject(DataUtils); + jest.spyOn(window, 'open').mockImplementation(() => null); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OrganizationDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load organization on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', OrganizationDetailComponent); + + // THEN + expect(instance.organization()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); + + describe('byteSize', () => { + it('Should call byteSize from DataUtils', () => { + // GIVEN + jest.spyOn(dataUtils, 'byteSize'); + const fakeBase64 = 'fake base64'; + + // WHEN + comp.byteSize(fakeBase64); + + // THEN + expect(dataUtils.byteSize).toHaveBeenCalledWith(fakeBase64); + }); + }); + + describe('openFile', () => { + it('Should call openFile from DataUtils', () => { + const newWindow = { ...window }; + newWindow.document.write = jest.fn(); + window.open = jest.fn(() => newWindow); + window.onload = jest.fn(() => newWindow) as any; + window.URL.createObjectURL = jest.fn() as any; + // GIVEN + jest.spyOn(dataUtils, 'openFile'); + const fakeContentType = 'fake content type'; + const fakeBase64 = 'fake base64'; + + // WHEN + comp.openFile(fakeBase64, fakeContentType); + + // THEN + expect(dataUtils.openFile).toHaveBeenCalledWith(fakeBase64, fakeContentType); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization/detail/organization-detail.component.ts b/src/main/webapp/app/entities/organization/detail/organization-detail.component.ts new file mode 100644 index 0000000..2362eb8 --- /dev/null +++ b/src/main/webapp/app/entities/organization/detail/organization-detail.component.ts @@ -0,0 +1,35 @@ +import { Component, inject, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { IOrganization } from '../organization.model'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; + +@Component({ + standalone: true, + selector: 'jhi-organization-detail', + templateUrl: './organization-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class OrganizationDetailComponent { + organization = input(null); + + // Needed to make the enum OrganizationNature accessible to the template + organizationNature = OrganizationNature; + + protected dataUtils = inject(DataUtils); + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.css b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.css new file mode 100644 index 0000000..e114f5e --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.css @@ -0,0 +1,16 @@ +:host { + display: inline-flex; + margin-left: 25px; +} + +.tree-btn-create { + margin-right: 10px; +} + +.tree-btn-create button.tree-btn-add { + border-radius: 10px 0px 0px 10px; +} + +.tree-btn-delete { + border-radius: 0px 10px 10px 0px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.html b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.html new file mode 100644 index 0000000..5d52848 --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.html @@ -0,0 +1,14 @@ + +
+ +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.spec.ts b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.spec.ts new file mode 100644 index 0000000..8ae812b --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OrganizationTreeActionsComponent } from './organization-tree-actions.component'; + +describe('OrganizationTreeActionsComponent', () => { + let component: OrganizationTreeActionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OrganizationTreeActionsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationTreeActionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.ts b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.ts new file mode 100644 index 0000000..eb5a842 --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization-tree-actions/organization-tree-actions.component.ts @@ -0,0 +1,80 @@ +import { Component, Input, OnInit, inject, EventEmitter, Output } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { combineLatest, filter, Observable, Subscription, tap, map } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; + +import { IOrganization } from '../../organization.model'; +import { OrganizationTypeService } from 'app/entities/organization-type/service/organization-type.service'; +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; + +interface TreeOption { + title: string; + routerLink: string[]; + queryParams: {}; +} + +interface TreeNode { + expandable: boolean; + name: string; + id: number; + level: number; + org: IOrganization; +} + +@Component({ + selector: 'organization-tree-actions', + standalone: true, + imports: [RouterModule,CommonModule,FontAwesomeModule,NgbDropdownModule,], + templateUrl: './organization-tree-actions.component.html', + styleUrl: './organization-tree-actions.component.css' +}) +export class OrganizationTreeActionsComponent implements OnInit { + // Input properties + @Input() node: any; + + // Evento emitter (for parent reaction) + @Output() actionDeletedEvent = new EventEmitter<{organization: IOrganization}>(); + + options: TreeOption[] = []; // List of options for this component instance + isDeleteDisabled: boolean = true; // Delete Action is initially disabled + + protected organizationTypeService = inject(OrganizationTypeService); + + ngOnInit(): void { + this.loadOptions(); + + if (this.node.expandable) { + this.isDeleteDisabled = true; + } else { + this.isDeleteDisabled = false; + } + } + + loadOptions(): void { + this.organizationTypeService + .queryByNature(this.node.org.organizationType.nature) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe({ + next: (res: IOrganizationType[]) => { + this.onResponseSuccess(res); + }, + }); + } + + delete(organization: IOrganization): void { + this.actionDeletedEvent.emit({organization}); // {organization: organization} is the same as {organization}. Its called a "Literal Shorthand Syntax", when the key name matches the name of the variable + } + + protected onResponseSuccess(res: IOrganizationType[]): void { + for (const r of res) { + this.options.push({ + title: r.name?r.name:"", + routerLink: ["/organization", 'new'], + queryParams: {parentOrganizationId:this.node.org.id, organizationTypeId:r.id}, + }); + } + } +} diff --git a/src/main/webapp/app/entities/organization/list/organization.component.html b/src/main/webapp/app/entities/organization/list/organization.component.html new file mode 100644 index 0000000..a793929 --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization.component.html @@ -0,0 +1,229 @@ + + +
+

+ Organizations + +
+ + + +
+

+ + + + + + @if (organizations?.length === 0) { +
+ Nenhum Organizations encontrado +
+ } + + @if (organizations && organizations.length > 0) { + + + + + + + + {{ orgType.description }} + {{node.name}} + + + + + + + + + {{ orgType.description }} + {{node.name}} + + + + + + } + + @if (false && organizations && organizations.length > 0) { +
+ + + + + + + + + + + + + + + @for (organization of organizations; track trackId) { + + + + + + + + + + + } + +
+
+ Organization Type + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Parent + +
+
+ @if (organization.organizationType) { + + } + {{ organization.code }}{{ organization.name }} + @if (organization.parent) { + + } + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/organization/list/organization.component.html.old b/src/main/webapp/app/entities/organization/list/organization.component.html.old new file mode 100644 index 0000000..c66013e --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization.component.html.old @@ -0,0 +1,174 @@ +
+

+ Organizations + +
+ + + +
+

+ + + + + + @if (organizations?.length === 0) { +
+ Nenhum Organizations encontrado +
+ } + + @if (organizations && organizations.length > 0) { +
+ + + + + + + + + + + + + + + + + + @for (organization of organizations; track trackId) { + + + + + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Image + + +
+
+
+ Input Inventory + + +
+
+
+ Output Inventory + + +
+
+
+ Version + + +
+
+
+ User + +
+
+
+ Parent + +
+
+
+ Organization Type + +
+
+ {{ organization.id }} + {{ organization.code }}{{ organization.name }} + @if (organization.image) { + + organization + + {{ organization.imageContentType }}, {{ byteSize(organization.image) }} + } + {{ organization.inputInventory }}{{ organization.outputInventory }}{{ organization.version }} + {{ organization.user?.login }} + + @if (organization.parent) { + + } + + @if (organization.organizationType) { + + } + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/organization/list/organization.component.html.old1 b/src/main/webapp/app/entities/organization/list/organization.component.html.old1 new file mode 100644 index 0000000..0a2dee8 --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization.component.html.old1 @@ -0,0 +1,204 @@ + +
+

+ Organizations + +
+ + + +
+

+ + + + + + @if (organizations?.length === 0) { +
+ Nenhum Organizations encontrado +
+ } + + @if (organizations && organizations.length > 0) { + + + + + + + + {{node.name}} + + + + + + + + + {{node.name}} + + + + + + } + + @if (organizations && organizations.length > 0) { +
+ + + + + + + + + + + + + + + + + + @for (organization of organizations; track trackId) { + + + + + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Image + + +
+
+
+ Input Inventory + + +
+
+
+ Output Inventory + + +
+
+
+ Version + + +
+
+
+ User + +
+
+
+ Parent + +
+
+
+ Organization Type + +
+
+ {{ organization.id }} + {{ organization.code }}{{ organization.name }} + @if (organization.image) { + + organization + + {{ organization.imageContentType }}, {{ byteSize(organization.image) }} + } + {{ organization.inputInventory }}{{ organization.outputInventory }}{{ organization.version }} + {{ organization.user?.login }} + + @if (organization.parent) { + + } + + @if (organization.organizationType) { + + } + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/organization/list/organization.component.spec.ts b/src/main/webapp/app/entities/organization/list/organization.component.spec.ts new file mode 100644 index 0000000..78b6bee --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../organization.test-samples'; +import { OrganizationService } from '../service/organization.service'; + +import { OrganizationComponent } from './organization.component'; +import SpyInstance = jest.SpyInstance; + +describe('Organization Management Component', () => { + let comp: OrganizationComponent; + let fixture: ComponentFixture; + let service: OrganizationService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, OrganizationComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(OrganizationComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationComponent); + comp = fixture.componentInstance; + service = TestBed.inject(OrganizationService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.organizations?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to organizationService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getOrganizationIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getOrganizationIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/organization/list/organization.component.ts b/src/main/webapp/app/entities/organization/list/organization.component.ts new file mode 100644 index 0000000..0693497 --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization.component.ts @@ -0,0 +1,309 @@ +/* TreeViewCustomizer */ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, concatMap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { IOrganization } from '../organization.model'; +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; +import { OrganizationTypeService } from 'app/entities/organization-type/service/organization-type.service'; +import { EntityArrayResponseType, OrganizationService } from '../service/organization.service'; +import { OrganizationDeleteDialogComponent } from '../delete/organization-delete-dialog.component'; + +/* Treeview - BEGIN */ +/* Treeview imports */ +/* See: https://way2rock00.medium.com/creating-dynamic-tree-using-mattree-in-angular-aabca88b3cfa */ +import { CommonModule } from '@angular/common'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import {MatSliderModule} from '@angular/material/slider'; +import {FlatTreeControl} from '@angular/cdk/tree'; +import {MatTreeFlatDataSource, MatTreeFlattener} from '@angular/material/tree'; + +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatIconModule} from '@angular/material/icon'; +import {MatButtonModule} from '@angular/material/button'; +import {MatTreeModule} from '@angular/material/tree'; + +import {OrganizationTreeActionsComponent} from './organization-tree-actions/organization-tree-actions.component'; + +interface TreeNodeFlat { + name: string; + Id: number; + parentId: number|null; + org: IOrganization; + children?: TreeNodeFlat[]; +} + +/** Flat node with expandable and level information */ +interface TreeNode { + expandable: boolean; + name: string; + id: number; + level: number; + org: IOrganization; +} + +interface TreeOption { + title: string; + routerLink: string[]; + queryParams: {}; +} +/* Treeview - END */ + +@Component({ + standalone: true, + selector: 'jhi-organization', + templateUrl: './organization.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + MatTreeModule, + MatButtonModule, + MatIconModule, + MatProgressBarModule, + NgbDropdownModule, + CommonModule, + OrganizationTreeActionsComponent, + ], +}) + +export class OrganizationComponent implements OnInit { + /* Treeview - BEGIN */ + private _transformer = (node: TreeNodeFlat, level: number) => { + return { + expandable: !!node.children && node.children.length > 0, + name: node.name, + id: node.Id, + level: level, + org: node.org + }; + } + + treeControl = new FlatTreeControl(node => node.level, node => node.expandable); + treeFlattener = new MatTreeFlattener(this._transformer, node => node.level, node => node.expandable, node => node.children); + dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + + hasChild = (_: number, node: TreeNode) => node.expandable; + showTreeActionsId: number = -1; + /* Treeview - END */ + + subscription: Subscription | null = null; + organizations?: IOrganization[]; + isLoading = false; + + organizationTypes?: IOrganizationType[]; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected organizationService = inject(OrganizationService); + protected organizationTypeService = inject(OrganizationTypeService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected dataUtils = inject(DataUtils); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IOrganization): number => this.organizationService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([ + this.activatedRoute.queryParamMap, + this.activatedRoute.data + ]).pipe( + // Load a single list of OrganizatonType's, to use as source for icons + concatMap(() => + this.organizationTypeService.query().pipe( + tap((types: HttpResponse) => { + if (types.body) { + this.organizationTypes = types.body; + } + }) + ) + ), + // Load and build the organization graph + tap(() => { + if (!this.organizations || this.organizations.length === 0) { + this.load(); + } + }) + ).subscribe(); + } + + + getType(org: IOrganization): IOrganizationType | undefined { + const selectedOrganizationType = this.organizationTypes?.find(type => type.id === org?.organizationType?.id); + return selectedOrganizationType; + } + + /* TreeView - BEGIN */ + //constructTree recursively iterates through the tree to create nested tree structure. + //We only need Id and parentId Columns in the flat data to construct this tree properly. + treeConstructOrg(treeData: IOrganization[]) { + let constructedTree: TreeNodeFlat[] = []; + + for (let i of treeData) { + let treeObjOrg = i; + let assigned = false; + + let treeObj: TreeNodeFlat = { + name: treeObjOrg.name as string, + Id: treeObjOrg.id, + parentId: treeObjOrg.parent==null?null:treeObjOrg.parent.id, + org: treeObjOrg + }; + + this.constructTree(constructedTree, treeObj, assigned) + } + + return constructedTree; + } + + treeConstruct(treeData: TreeNodeFlat[]) { + let constructedTree: TreeNodeFlat[] = []; + for (let i of treeData) { + let treeObj = i; + let assigned = false; + this.constructTree(constructedTree, treeObj, assigned) + } + return constructedTree; + } + + /** + * Recursive method to build the tree. Pay attention to the constructedTree: any arg. + * This will have 2 types: TreeNodeFlat[] OR TreeNodeFlat. Depending on the level being evaluated, + * the arg might be the all tree (TreeNodeFlat[]) or a branch of the tree (TreeNodeFlat). + */ + constructTree(constructedTree: any, treeObj: TreeNodeFlat, assigned: boolean) { + if (treeObj.parentId == null) { + treeObj.children = []; + constructedTree.push(treeObj); + return true; + } else if (treeObj.parentId == constructedTree.Id) { + treeObj.children = []; + constructedTree.children.push(treeObj); + return true; + } + else { + if (constructedTree.children != undefined) { + for (let index = 0; index < constructedTree.children.length; index++) { + let constructedObj = constructedTree.children[index]; + if (assigned == false) { + assigned = this.constructTree(constructedObj, treeObj, assigned); + } + } + } else { + for (let index = 0; index < constructedTree.length; index++) { + let constructedObj = constructedTree[index]; + if (assigned == false) { + assigned = this.constructTree(constructedObj, treeObj, assigned); + } + } + } + return false; + } + } + + onMouseEnter(node: TreeNode): void { + this.showTreeActionsId = node.id; + //console.log('On Mouse Enter '); + } + + onMouseLeave(node: TreeNode): void { + this.showTreeActionsId = -1; + // console.log('On Mouse Leave'); + } + + protected handleActionDeletedEvent(data: { organization: IOrganization }): void { + this.delete(data.organization); + } + /* TreeView - END */ + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + return this.dataUtils.openFile(base64String, contentType); + } + + delete(organization: IOrganization): void { + const modalRef = this.modalService.open(OrganizationDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.organization = organization; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + +/* + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } +*/ + protected onResponseSuccess(response: EntityArrayResponseType): void { + // const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + // this.organizations = this.refineData(dataFromBody); + this.organizations = this.fillComponentAttributesFromResponseBody(response.body); + this.dataSource.data = this.treeConstructOrg(this.organizations as IOrganization[]); /* TreeView - Load tree*/ + + //Auto-Expand the first level + this.treeControl.dataNodes + .filter(node => node.level === 0) // Only expand root-level nodes + .forEach(node => this.treeControl.expand(node)); + } + + protected fillComponentAttributesFromResponseBody(data: IOrganization[] | null): IOrganization[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.organizationService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/organization/list/organization.component.ts.old b/src/main/webapp/app/entities/organization/list/organization.component.ts.old new file mode 100644 index 0000000..cadc0a5 --- /dev/null +++ b/src/main/webapp/app/entities/organization/list/organization.component.ts.old @@ -0,0 +1,132 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { IOrganization } from '../organization.model'; +import { EntityArrayResponseType, OrganizationService } from '../service/organization.service'; +import { OrganizationDeleteDialogComponent } from '../delete/organization-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-organization', + templateUrl: './organization.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class OrganizationComponent implements OnInit { + subscription: Subscription | null = null; + organizations?: IOrganization[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected organizationService = inject(OrganizationService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected dataUtils = inject(DataUtils); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IOrganization): number => this.organizationService.getOrganizationIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.organizations || this.organizations.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + return this.dataUtils.openFile(base64String, contentType); + } + + delete(organization: IOrganization): void { + const modalRef = this.modalService.open(OrganizationDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.organization = organization; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.organizations = this.refineData(dataFromBody); + } + + protected refineData(data: IOrganization[]): IOrganization[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IOrganization[] | null): IOrganization[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.organizationService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/organization/organization.model.ts b/src/main/webapp/app/entities/organization/organization.model.ts new file mode 100644 index 0000000..2e28e76 --- /dev/null +++ b/src/main/webapp/app/entities/organization/organization.model.ts @@ -0,0 +1,19 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IUser } from 'app/entities/user/user.model'; +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; + +export interface IOrganization extends IResilientEntity { + code?: string | null; + name?: string | null; + image?: string | null; + imageContentType?: string | null; + inputInventory?: boolean | null; + outputInventory?: boolean | null; + user?: Pick | null; + parent?: Pick | null; + organizationType?: Pick | null; + sort?: number | null; +} + +export type NewOrganization = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/organization/organization.routes.ts b/src/main/webapp/app/entities/organization/organization.routes.ts new file mode 100644 index 0000000..96b146c --- /dev/null +++ b/src/main/webapp/app/entities/organization/organization.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { OrganizationComponent } from './list/organization.component'; +import { OrganizationDetailComponent } from './detail/organization-detail.component'; +import { OrganizationUpdateComponent } from './update/organization-update.component'; +import OrganizationResolve from './route/organization-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { OrganizationService } from './service/organization.service'; +import { Resources } from 'app/security/resources.model'; + +const organizationRoute: Routes = [ + { + path: '', + component: OrganizationComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: OrganizationService, + resource: Resources.ORGANIZATION, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: OrganizationDetailComponent, + resolve: { + organization: OrganizationResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: OrganizationService, + resource: Resources.ORGANIZATION, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: OrganizationUpdateComponent, + resolve: { + organization: OrganizationResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: OrganizationService, + resource: Resources.ORGANIZATION, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: OrganizationUpdateComponent, + resolve: { + organization: OrganizationResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: OrganizationService, + resource: Resources.ORGANIZATION, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default organizationRoute; diff --git a/src/main/webapp/app/entities/organization/organization.test-samples.ts b/src/main/webapp/app/entities/organization/organization.test-samples.ts new file mode 100644 index 0000000..8445bbc --- /dev/null +++ b/src/main/webapp/app/entities/organization/organization.test-samples.ts @@ -0,0 +1,38 @@ +import { IOrganization, NewOrganization } from './organization.model'; + +export const sampleWithRequiredData: IOrganization = { + id: 19700, + code: 'that beyond appal', + name: 'lure amazing painfully', +}; + +export const sampleWithPartialData: IOrganization = { + id: 14936, + code: 'studious', + name: 'cuss bah fossick', + image: '../fake-data/blob/hipster.png', + imageContentType: 'unknown', + inputInventory: false, +}; + +export const sampleWithFullData: IOrganization = { + id: 14523, + code: 'but eventually', + name: 'straighten spotless and', + image: '../fake-data/blob/hipster.png', + imageContentType: 'unknown', + inputInventory: false, + outputInventory: true, + version: 25234, +}; + +export const sampleWithNewData: NewOrganization = { + code: 'since jumbo', + name: 'across woot', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/organization/route/organization-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/organization/route/organization-routing-resolve.service.spec.ts new file mode 100644 index 0000000..5adea20 --- /dev/null +++ b/src/main/webapp/app/entities/organization/route/organization-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IOrganization } from '../organization.model'; +import { OrganizationService } from '../service/organization.service'; + +import organizationResolve from './organization-routing-resolve.service'; + +describe('Organization routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: OrganizationService; + let resultOrganization: IOrganization | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(OrganizationService); + resultOrganization = undefined; + }); + + describe('resolve', () => { + it('should return IOrganization returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + organizationResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOrganization = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultOrganization).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + organizationResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOrganization = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultOrganization).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + organizationResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOrganization = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultOrganization).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization/route/organization-routing-resolve.service.ts b/src/main/webapp/app/entities/organization/route/organization-routing-resolve.service.ts new file mode 100644 index 0000000..e6996c1 --- /dev/null +++ b/src/main/webapp/app/entities/organization/route/organization-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IOrganization } from '../organization.model'; +import { OrganizationService } from '../service/organization.service'; + +const organizationResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(OrganizationService) + .find(id) + .pipe( + mergeMap((organization: HttpResponse) => { + if (organization.body) { + return of(organization.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default organizationResolve; diff --git a/src/main/webapp/app/entities/organization/service/organization.service.spec.ts b/src/main/webapp/app/entities/organization/service/organization.service.spec.ts new file mode 100644 index 0000000..4dbe69e --- /dev/null +++ b/src/main/webapp/app/entities/organization/service/organization.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IOrganization } from '../organization.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../organization.test-samples'; + +import { OrganizationService } from './organization.service'; + +const requireRestSample: IOrganization = { + ...sampleWithRequiredData, +}; + +describe('Organization Service', () => { + let service: OrganizationService; + let httpMock: HttpTestingController; + let expectedResult: IOrganization | IOrganization[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(OrganizationService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a Organization', () => { + const organization = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(organization).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a Organization', () => { + const organization = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(organization).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a Organization', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of Organization', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a Organization', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addOrganizationToCollectionIfMissing', () => { + it('should add a Organization to an empty array', () => { + const organization: IOrganization = sampleWithRequiredData; + expectedResult = service.addOrganizationToCollectionIfMissing([], organization); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(organization); + }); + + it('should not add a Organization to an array that contains it', () => { + const organization: IOrganization = sampleWithRequiredData; + const organizationCollection: IOrganization[] = [ + { + ...organization, + }, + sampleWithPartialData, + ]; + expectedResult = service.addOrganizationToCollectionIfMissing(organizationCollection, organization); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a Organization to an array that doesn't contain it", () => { + const organization: IOrganization = sampleWithRequiredData; + const organizationCollection: IOrganization[] = [sampleWithPartialData]; + expectedResult = service.addOrganizationToCollectionIfMissing(organizationCollection, organization); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(organization); + }); + + it('should add only unique Organization to an array', () => { + const organizationArray: IOrganization[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const organizationCollection: IOrganization[] = [sampleWithRequiredData]; + expectedResult = service.addOrganizationToCollectionIfMissing(organizationCollection, ...organizationArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const organization: IOrganization = sampleWithRequiredData; + const organization2: IOrganization = sampleWithPartialData; + expectedResult = service.addOrganizationToCollectionIfMissing([], organization, organization2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(organization); + expect(expectedResult).toContain(organization2); + }); + + it('should accept null and undefined values', () => { + const organization: IOrganization = sampleWithRequiredData; + expectedResult = service.addOrganizationToCollectionIfMissing([], null, organization, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(organization); + }); + + it('should return initial array if no Organization is added', () => { + const organizationCollection: IOrganization[] = [sampleWithRequiredData]; + expectedResult = service.addOrganizationToCollectionIfMissing(organizationCollection, undefined, null); + expect(expectedResult).toEqual(organizationCollection); + }); + }); + + describe('compareOrganization', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareOrganization(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareOrganization(entity1, entity2); + const compareResult2 = service.compareOrganization(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareOrganization(entity1, entity2); + const compareResult2 = service.compareOrganization(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareOrganization(entity1, entity2); + const compareResult2 = service.compareOrganization(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/organization/service/organization.service.ts b/src/main/webapp/app/entities/organization/service/organization.service.ts new file mode 100644 index 0000000..b8279fd --- /dev/null +++ b/src/main/webapp/app/entities/organization/service/organization.service.ts @@ -0,0 +1,57 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IOrganization, NewOrganization } from '../organization.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class OrganizationService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/organizations'); + getResourceUrl(): string { return this.resourceUrl; } + + /** + * Get all Organizations with the given nature + * @param nature + * @returns + */ + queryByNature(nature: string): Observable { + return this.http.get(`${this.resourceUrl}/byNature/${nature}`, { observe: 'response' }); + } + + /** + * Get all Organizations with the given nature, restricted to Organizations that allow inputInventory==TRUE + * @param nature + * @returns + */ + queryByNatureForInput(nature: string): Observable { + return this.http.get(`${this.resourceUrl}/byNature/forInput/${nature}`, { observe: 'response' }); + } + + /** + * Get all Organizations with the given nature, within the user access hierarchy + * @param nature + * @returns + */ + queryByNatureAndHierarchy(nature: string): Observable { + return this.http.get(`${this.resourceUrl}/hierarchy/${nature}`, { observe: 'response' }); + } + + /** + * Get all Organizations with the given nature, within the user access hierarchy, restricted to Organizations that allow inputInventory==TRUE + * @param nature + * @returns + */ + queryByNatureAndHierarchyForInput(nature: string): Observable { + return this.http.get(`${this.resourceUrl}/hierarchy/forInput/${nature}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/organization/update/organization-form.service.spec.ts b/src/main/webapp/app/entities/organization/update/organization-form.service.spec.ts new file mode 100644 index 0000000..dd133bb --- /dev/null +++ b/src/main/webapp/app/entities/organization/update/organization-form.service.spec.ts @@ -0,0 +1,102 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../organization.test-samples'; + +import { OrganizationFormService } from './organization-form.service'; + +describe('Organization Form Service', () => { + let service: OrganizationFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OrganizationFormService); + }); + + describe('Service methods', () => { + describe('createOrganizationFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createOrganizationFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + image: expect.any(Object), + inputInventory: expect.any(Object), + outputInventory: expect.any(Object), + version: expect.any(Object), + user: expect.any(Object), + parent: expect.any(Object), + organizationType: expect.any(Object), + }), + ); + }); + + it('passing IOrganization should create a new form with FormGroup', () => { + const formGroup = service.createOrganizationFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + image: expect.any(Object), + inputInventory: expect.any(Object), + outputInventory: expect.any(Object), + version: expect.any(Object), + user: expect.any(Object), + parent: expect.any(Object), + organizationType: expect.any(Object), + }), + ); + }); + }); + + describe('getOrganization', () => { + it('should return NewOrganization for default Organization initial value', () => { + const formGroup = service.createOrganizationFormGroup(sampleWithNewData); + + const organization = service.getOrganization(formGroup) as any; + + expect(organization).toMatchObject(sampleWithNewData); + }); + + it('should return NewOrganization for empty Organization initial value', () => { + const formGroup = service.createOrganizationFormGroup(); + + const organization = service.getOrganization(formGroup) as any; + + expect(organization).toMatchObject({}); + }); + + it('should return IOrganization', () => { + const formGroup = service.createOrganizationFormGroup(sampleWithRequiredData); + + const organization = service.getOrganization(formGroup) as any; + + expect(organization).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IOrganization should not enable id FormControl', () => { + const formGroup = service.createOrganizationFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewOrganization should disable id FormControl', () => { + const formGroup = service.createOrganizationFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization/update/organization-form.service.ts b/src/main/webapp/app/entities/organization/update/organization-form.service.ts new file mode 100644 index 0000000..38de6db --- /dev/null +++ b/src/main/webapp/app/entities/organization/update/organization-form.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IOrganization, NewOrganization } from '../organization.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IOrganization for edit and NewOrganizationFormGroupInput for create. + */ +type OrganizationFormGroupInput = IOrganization | PartialWithRequiredKeyOf; + +type OrganizationFormDefaults = Pick; + +type OrganizationFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + image: FormControl; + imageContentType: FormControl; + inputInventory: FormControl; + outputInventory: FormControl; + version: FormControl; + sort: FormControl; + user: FormControl; + parent: FormControl; + organizationType: FormControl; +}; + +export type OrganizationFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class OrganizationFormService { + createOrganizationFormGroup(organization: OrganizationFormGroupInput = { id: null }): OrganizationFormGroup { + const organizationRawValue = { + ...this.getFormDefaults(), + ...organization, + }; + return new FormGroup({ + id: new FormControl( + { value: organizationRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(organizationRawValue.code, { + validators: [Validators.required, Validators.minLength(3)], + }), + name: new FormControl(organizationRawValue.name, { + validators: [Validators.required, Validators.minLength(3)], + }), + image: new FormControl(organizationRawValue.image), + imageContentType: new FormControl(organizationRawValue.imageContentType), + inputInventory: new FormControl(organizationRawValue.inputInventory), + outputInventory: new FormControl(organizationRawValue.outputInventory), + version: new FormControl(organizationRawValue.version), + sort: new FormControl(organizationRawValue.sort), + user: new FormControl(organizationRawValue.user), + parent: new FormControl(organizationRawValue.parent), + organizationType: new FormControl(organizationRawValue.organizationType), + }); + } + + getOrganization(form: OrganizationFormGroup): IOrganization | NewOrganization { + return form.getRawValue() as IOrganization | NewOrganization; + } + + resetForm(form: OrganizationFormGroup, organization: OrganizationFormGroupInput): void { + const organizationRawValue = { ...this.getFormDefaults(), ...organization }; + form.reset( + { + ...organizationRawValue, + id: { value: organizationRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): OrganizationFormDefaults { + return { + id: null, + inputInventory: false, + outputInventory: false, + }; + } +} diff --git a/src/main/webapp/app/entities/organization/update/organization-update.component.html b/src/main/webapp/app/entities/organization/update/organization-update.component.html new file mode 100644 index 0000000..6ecfca3 --- /dev/null +++ b/src/main/webapp/app/entities/organization/update/organization-update.component.html @@ -0,0 +1,215 @@ + +
+
+
+

+ Criar ou editar Organization +

+ +
+ +
+ + @if (editForm.controls.id.value == null && parentOrganizationDefault == null) { + + } @else if (parentOrganizationDefault != null) { + + } @else { + + } + +
+
+ + @if (editForm.controls.id.value == null && organizationTypeDefault == null) { + + } @else if (organizationTypeDefault != null) { + + } @else { + + } + +
+
+ + + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('name')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ +
+ @if (editForm.get('image')!.value) { + organization + } + @if (editForm.get('image')!.value) { +
+ {{ editForm.get('imageContentType')!.value }}, {{ byteSize(editForm.get('image')!.value!) }} + +
+ } + +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/organization/update/organization-update.component.html.old b/src/main/webapp/app/entities/organization/update/organization-update.component.html.old new file mode 100644 index 0000000..dc8f05c --- /dev/null +++ b/src/main/webapp/app/entities/organization/update/organization-update.component.html.old @@ -0,0 +1,212 @@ +
+
+
+

+ Criar ou editar Organization +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('name')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+ +
+ +
+ @if (editForm.get('image')!.value) { + organization + } + @if (editForm.get('image')!.value) { +
+ {{ editForm.get('imageContentType')!.value }}, {{ byteSize(editForm.get('image')!.value!) }} + +
+ } + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/organization/update/organization-update.component.spec.ts b/src/main/webapp/app/entities/organization/update/organization-update.component.spec.ts new file mode 100644 index 0000000..afa4ae9 --- /dev/null +++ b/src/main/webapp/app/entities/organization/update/organization-update.component.spec.ts @@ -0,0 +1,238 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IUser } from 'app/entities/user/user.model'; +import { UserService } from 'app/entities/user/service/user.service'; +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; +import { OrganizationTypeService } from 'app/entities/organization-type/service/organization-type.service'; +import { IOrganization } from '../organization.model'; +import { OrganizationService } from '../service/organization.service'; +import { OrganizationFormService } from './organization-form.service'; + +import { OrganizationUpdateComponent } from './organization-update.component'; + +describe('Organization Management Update Component', () => { + let comp: OrganizationUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let organizationFormService: OrganizationFormService; + let organizationService: OrganizationService; + let userService: UserService; + let organizationTypeService: OrganizationTypeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, OrganizationUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(OrganizationUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + organizationFormService = TestBed.inject(OrganizationFormService); + organizationService = TestBed.inject(OrganizationService); + userService = TestBed.inject(UserService); + organizationTypeService = TestBed.inject(OrganizationTypeService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call User query and add missing value', () => { + const organization: IOrganization = { id: 456 }; + const user: IUser = { id: 8047 }; + organization.user = user; + + const userCollection: IUser[] = [{ id: 10715 }]; + jest.spyOn(userService, 'query').mockReturnValue(of(new HttpResponse({ body: userCollection }))); + const additionalUsers = [user]; + const expectedCollection: IUser[] = [...additionalUsers, ...userCollection]; + jest.spyOn(userService, 'addUserToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ organization }); + comp.ngOnInit(); + + expect(userService.query).toHaveBeenCalled(); + expect(userService.addUserToCollectionIfMissing).toHaveBeenCalledWith( + userCollection, + ...additionalUsers.map(expect.objectContaining), + ); + expect(comp.usersSharedCollection).toEqual(expectedCollection); + }); + + it('Should call Organization query and add missing value', () => { + const organization: IOrganization = { id: 456 }; + const parent: IOrganization = { id: 8967 }; + organization.parent = parent; + + const organizationCollection: IOrganization[] = [{ id: 31072 }]; + jest.spyOn(organizationService, 'query').mockReturnValue(of(new HttpResponse({ body: organizationCollection }))); + const additionalOrganizations = [parent]; + const expectedCollection: IOrganization[] = [...additionalOrganizations, ...organizationCollection]; + jest.spyOn(organizationService, 'addOrganizationToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ organization }); + comp.ngOnInit(); + + expect(organizationService.query).toHaveBeenCalled(); + expect(organizationService.addOrganizationToCollectionIfMissing).toHaveBeenCalledWith( + organizationCollection, + ...additionalOrganizations.map(expect.objectContaining), + ); + expect(comp.organizationsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call OrganizationType query and add missing value', () => { + const organization: IOrganization = { id: 456 }; + const organizationType: IOrganizationType = { id: 17558 }; + organization.organizationType = organizationType; + + const organizationTypeCollection: IOrganizationType[] = [{ id: 24942 }]; + jest.spyOn(organizationTypeService, 'query').mockReturnValue(of(new HttpResponse({ body: organizationTypeCollection }))); + const additionalOrganizationTypes = [organizationType]; + const expectedCollection: IOrganizationType[] = [...additionalOrganizationTypes, ...organizationTypeCollection]; + jest.spyOn(organizationTypeService, 'addOrganizationTypeToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ organization }); + comp.ngOnInit(); + + expect(organizationTypeService.query).toHaveBeenCalled(); + expect(organizationTypeService.addOrganizationTypeToCollectionIfMissing).toHaveBeenCalledWith( + organizationTypeCollection, + ...additionalOrganizationTypes.map(expect.objectContaining), + ); + expect(comp.organizationTypesSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const organization: IOrganization = { id: 456 }; + const user: IUser = { id: 8950 }; + organization.user = user; + const parent: IOrganization = { id: 28186 }; + organization.parent = parent; + const organizationType: IOrganizationType = { id: 27773 }; + organization.organizationType = organizationType; + + activatedRoute.data = of({ organization }); + comp.ngOnInit(); + + expect(comp.usersSharedCollection).toContain(user); + expect(comp.organizationsSharedCollection).toContain(parent); + expect(comp.organizationTypesSharedCollection).toContain(organizationType); + expect(comp.organization).toEqual(organization); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const organization = { id: 123 }; + jest.spyOn(organizationFormService, 'getOrganization').mockReturnValue(organization); + jest.spyOn(organizationService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ organization }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: organization })); + saveSubject.complete(); + + // THEN + expect(organizationFormService.getOrganization).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(organizationService.update).toHaveBeenCalledWith(expect.objectContaining(organization)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const organization = { id: 123 }; + jest.spyOn(organizationFormService, 'getOrganization').mockReturnValue({ id: null }); + jest.spyOn(organizationService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ organization: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: organization })); + saveSubject.complete(); + + // THEN + expect(organizationFormService.getOrganization).toHaveBeenCalled(); + expect(organizationService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const organization = { id: 123 }; + jest.spyOn(organizationService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ organization }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(organizationService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareUser', () => { + it('Should forward to userService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(userService, 'compareUser'); + comp.compareUser(entity, entity2); + expect(userService.compareUser).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareOrganization', () => { + it('Should forward to organizationService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(organizationService, 'compareOrganization'); + comp.compareOrganization(entity, entity2); + expect(organizationService.compareOrganization).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareOrganizationType', () => { + it('Should forward to organizationTypeService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(organizationTypeService, 'compareOrganizationType'); + comp.compareOrganizationType(entity, entity2); + expect(organizationTypeService.compareOrganizationType).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/organization/update/organization-update.component.ts b/src/main/webapp/app/entities/organization/update/organization-update.component.ts new file mode 100644 index 0000000..202a122 --- /dev/null +++ b/src/main/webapp/app/entities/organization/update/organization-update.component.ts @@ -0,0 +1,230 @@ +import { Component, inject, OnInit, ElementRef } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { AlertError } from 'app/shared/alert/alert-error.model'; +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; +import { DataUtils, FileLoadError } from 'app/core/util/data-util.service'; +import { IUser } from 'app/entities/user/user.model'; +import { UserService } from 'app/entities/user/service/user.service'; +import { IOrganizationType } from 'app/entities/organization-type/organization-type.model'; +import { OrganizationTypeService } from 'app/entities/organization-type/service/organization-type.service'; +import { OrganizationService } from '../service/organization.service'; +import { IOrganization } from '../organization.model'; +import { OrganizationFormService, OrganizationFormGroup } from './organization-form.service'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; + +@Component({ + standalone: true, + selector: 'jhi-organization-update', + templateUrl: './organization-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class OrganizationUpdateComponent implements OnInit { + // Properties with defaults by router + parentOrganizationDefault : IOrganization | null = null; + organizationTypeDefault : IOrganizationType | null = null; + + // Needed to make the enum OrganizationNature accessible to the template + organizationNature = OrganizationNature; + + isSaving = false; + organization: IOrganization | null = null; + + usersSharedCollection: IUser[] = []; + organizationsSharedCollection: IOrganization[] = []; + organizationTypesSharedCollection: IOrganizationType[] = []; + + protected dataUtils = inject(DataUtils); + protected eventManager = inject(EventManager); + protected organizationService = inject(OrganizationService); + protected organizationFormService = inject(OrganizationFormService); + protected userService = inject(UserService); + protected organizationTypeService = inject(OrganizationTypeService); + protected elementRef = inject(ElementRef); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: OrganizationFormGroup = this.organizationFormService.createOrganizationFormGroup(); + + compareUser = (o1: IUser | null, o2: IUser | null): boolean => this.userService.compareUser(o1, o2); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + compareOrganizationType = (o1: IOrganizationType | null, o2: IOrganizationType | null): boolean => + this.organizationTypeService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ organization }) => { + this.organization = organization; + if (organization) { + // Set defaults. Important for html form behavior + this.parentOrganizationDefault = organization.parent; + this.organizationTypeDefault = organization.organizationType; + + // Editing an existing Organization + this.updateForm(organization); + this.loadRelationshipsOptions(); // Load all relations + } else { + // Create a new Organization + // Router parameter's ? + this.activatedRoute.queryParams.subscribe(params => { + const parentOrganizationId = params['parentOrganizationId']; + if (parentOrganizationId) { + // Load the Parent Organization list (only this one) + this.loadRelationshipsOptionsOrganization(Number(parentOrganizationId)); + } else { + // No default. Load the list + this.loadRelationshipsOptionsOrganization(null); + } + + const organizationTypeId = params['organizationTypeId']; + if (organizationTypeId) { + // Load the OrganizationType list (only this one) + this.loadRelationshipsOptionsOrganizationType(Number(organizationTypeId)); + } else { + // No default. Load the list + this.loadRelationshipsOptionsOrganizationType(null); + } + + }); + } + + this.loadRelationshipsOptions(); + }); + } + + byteSize(base64String: string): string { + return this.dataUtils.byteSize(base64String); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + setFileData(event: Event, field: string, isImage: boolean): void { + this.dataUtils.loadFileToForm(event, this.editForm, field, isImage).subscribe({ + error: (err: FileLoadError) => + this.eventManager.broadcast(new EventWithContent('resilientApp.error', { ...err, key: 'error.file.' + err.key })), + }); + } + + clearInputImage(field: string, fieldContentType: string, idInput: string): void { + this.editForm.patchValue({ + [field]: null, + [fieldContentType]: null, + }); + if (idInput && this.elementRef.nativeElement.querySelector('#' + idInput)) { + this.elementRef.nativeElement.querySelector('#' + idInput).value = null; + } + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const organization = this.organizationFormService.getOrganization(this.editForm); + if (organization.id !== null) { + this.subscribeToSaveResponse(this.organizationService.update(organization)); + } else { + this.subscribeToSaveResponse(this.organizationService.create(organization)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(organization: IOrganization): void { + this.organization = organization; + this.organizationFormService.resetForm(this.editForm, organization); + + this.usersSharedCollection = this.userService.addUserToCollectionIfMissing(this.usersSharedCollection, organization.user); + this.organizationsSharedCollection = this.organizationService.addEntityToCollectionIfMissing( + this.organizationsSharedCollection, + organization.parent, + ); + this.organizationTypesSharedCollection = this.organizationTypeService.addEntityToCollectionIfMissing( + this.organizationTypesSharedCollection, + organization.organizationType, + ); + } + + protected loadRelationshipsOptions(): void { + this.loadRelationshipsOptionsUser(); + this.loadRelationshipsOptionsOrganization(null); + this.loadRelationshipsOptionsOrganizationType(null); + } + + protected loadRelationshipsOptionsUser(): void { + this.userService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe(map((users: IUser[]) => this.userService.addUserToCollectionIfMissing(users, this.organization?.user))) + .subscribe((users: IUser[]) => (this.usersSharedCollection = users)); + } + + protected loadRelationshipsOptionsOrganization(parentOrganizationId: number | null): void { + this.organizationService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((organizations: IOrganization[]) => + this.organizationService.addEntityToCollectionIfMissing(organizations, this.organization?.parent), + ), + ) + .subscribe((organizations: IOrganization[]) => { + this.organizationsSharedCollection = organizations; + + if (parentOrganizationId) { + this.parentOrganizationDefault = this.organizationsSharedCollection.find(org => org.id === parentOrganizationId) ?? null; + this.editForm.patchValue({ parent: this.parentOrganizationDefault }); + } + }); + } + + protected loadRelationshipsOptionsOrganizationType(organizationTypeId: number | null): void { + this.organizationTypeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((organizationTypes: IOrganizationType[]) => + this.organizationTypeService.addEntityToCollectionIfMissing( + organizationTypes, + this.organization?.organizationType, + ), + ), + ) + .subscribe((organizationTypes: IOrganizationType[]) => { + this.organizationTypesSharedCollection = organizationTypes; + + if (organizationTypeId) { + this.organizationTypeDefault = this.organizationTypesSharedCollection.find(orgType => orgType.id === organizationTypeId) ?? null; + this.editForm.patchValue({ organizationType: this.organizationTypeDefault }); + } + }); + } +} diff --git a/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.html b/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.html new file mode 100644 index 0000000..839e5a5 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.html @@ -0,0 +1,72 @@ +
+
+ @if (outputData()) { +
+

Output Data

+ +
+ + + + + +
+
Código
+
+ {{ outputData()!.id }} +
+
+ Value +
+
+ {{ outputData()!.value }} +
+
+ Version +
+
+ {{ outputData()!.version }} +
+
Variable
+
+ @if (outputData()!.variable) { + + } +
+
Period
+
+ @if (outputData()!.period) { + + } +
+
Period Version
+
+ @if (outputData()!.periodVersion) { + + } +
+
Base Unit
+
+ @if (outputData()!.baseUnit) { + + } +
+
+ + +
+ } +
+
diff --git a/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.spec.ts b/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.spec.ts new file mode 100644 index 0000000..86e78fa --- /dev/null +++ b/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { OutputDataDetailComponent } from './output-data-detail.component'; + +describe('OutputData Management Detail Component', () => { + let comp: OutputDataDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OutputDataDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: OutputDataDetailComponent, + resolve: { outputData: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(OutputDataDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OutputDataDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load outputData on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', OutputDataDetailComponent); + + // THEN + expect(instance.outputData()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.ts b/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.ts new file mode 100644 index 0000000..f1d94ff --- /dev/null +++ b/src/main/webapp/app/entities/output-data/detail/output-data-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IOutputData } from '../output-data.model'; + +@Component({ + standalone: true, + selector: 'jhi-output-data-detail', + templateUrl: './output-data-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class OutputDataDetailComponent { + outputData = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/output-data/list/output-data-search-form.service.spec.ts b/src/main/webapp/app/entities/output-data/list/output-data-search-form.service.spec.ts new file mode 100644 index 0000000..143f72d --- /dev/null +++ b/src/main/webapp/app/entities/output-data/list/output-data-search-form.service.spec.ts @@ -0,0 +1,126 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../input-data.test-samples'; + +import { OutputDataSearchFormService } from './output-data-search-form.service'; + +describe('OutputData Search Form Service', () => { + let service: OutputDataSearchFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OutputDataSearchFormService); + }); + + describe('Service methods', () => { + describe('createOutputDatasearchFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createOutputDataSearchFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + + it('passing SearchOutputData should create a new form with FormGroup', () => { + const formGroup = service.createOutputDataSearchFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + }); + + describe('getOutputData', () => { + it('should return SearchOutputData for default OutputData initial value', () => { + const formGroup = service.createOutputDataSearchFormGroup(sampleWithNewData); + + const outputData = service.getOutputData(formGroup) as any; + + expect(outputData).toMatchObject(sampleWithNewData); + }); + + it('should return SearchOutputData for empty OutputData initial value', () => { + const formGroup = service.createOutputDataSearchFormGroup(); + + const outputData = service.getOutputData(formGroup) as any; + + expect(outputData).toMatchObject({}); + }); + + it('should return SearchOutputData', () => { + const formGroup = service.createOutputDataSearchFormGroup(sampleWithRequiredData); + + const outputData = service.getOutputData(formGroup) as any; + + expect(outputData).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IOutputData should not enable id FormControl', () => { + const formGroup = service.createOutputDataSearchFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing SearchOutputData should disable id FormControl', () => { + const formGroup = service.createOutputDataSearchFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/output-data/list/output-data-search-form.service.ts b/src/main/webapp/app/entities/output-data/list/output-data-search-form.service.ts new file mode 100644 index 0000000..b54f110 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/list/output-data-search-form.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { SearchOutputData } from '../output-data.model'; +import { SearchFormService } from 'app/entities/shared/shared-search-form.service'; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts SearchOutputData, for search. in the list + */ +type OutputDataSearchFormGroupInput = SearchOutputData; + +type OutputDataSearchFormDefaults = SearchOutputData; + +type OutputDataSearchFormGroupContent = { + id: FormControl; + variableId: FormControl; + variableNameOrCode: FormControl; + periodId: FormControl; + periodVersionId: FormControl; + ownerId: FormControl; + variableScopeId: FormControl; + variableCategoryId: FormControl; +}; + +export type OutputDataSearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class OutputDataSearchFormService extends SearchFormService { + createOutputDataSearchFormGroup(outputData: OutputDataSearchFormGroupInput = { id: null }): OutputDataSearchFormGroup { + const outputDataRawValue = { + ...this.getSearchFormDefaults(), + ...outputData, + }; + return new FormGroup({ + id: new FormControl(outputDataRawValue.id), + variableId: new FormControl(outputDataRawValue.variableId), + variableNameOrCode: new FormControl(outputDataRawValue.variableNameOrCode), + periodId: new FormControl(outputDataRawValue.periodId), + periodVersionId: new FormControl(outputDataRawValue.periodVersionId), + ownerId: new FormControl(outputDataRawValue.ownerId), + variableScopeId: new FormControl(outputDataRawValue.variableScopeId), + variableCategoryId: new FormControl(outputDataRawValue.variableCategoryId), + }); + } + + getOutputData(form: OutputDataSearchFormGroup): SearchOutputData { + return form.getRawValue() as SearchOutputData; + } + + resetForm(form: OutputDataSearchFormGroup, outputData: OutputDataSearchFormGroupInput): void { + const outputDataRawValue = { ...this.getSearchFormDefaults(), ...outputData }; + form.reset( + { + ...outputDataRawValue, + id: { value: outputDataRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): OutputDataSearchFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/output-data/list/output-data.component.css b/src/main/webapp/app/entities/output-data/list/output-data.component.css new file mode 100644 index 0000000..e3a0c95 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/list/output-data.component.css @@ -0,0 +1,37 @@ +.form-group { + display: flex; + flex-direction: column; +} + +.search-form label { + font-weight: bold; + margin-bottom: 5px; +} + +.search-form select, .search-form button { + padding: 8px; + font-size: 16px; + width: 100%; /* Makes sure they fill the column */ +} + +.search-form button { + cursor: pointer; + background-color: #007BFF; + color: white; + border: none; + padding: 10px; + font-weight: bold; +} + +.search-form button:hover { + background-color: #0056b3; +} + +.search-form .btn-refresh { + min-width: 200px; + margin: 0px; +} + +#searchForm .form-group { + width: 100%; +} diff --git a/src/main/webapp/app/entities/output-data/list/output-data.component.html b/src/main/webapp/app/entities/output-data/list/output-data.component.html new file mode 100644 index 0000000..48b3381 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/list/output-data.component.html @@ -0,0 +1,219 @@ +
+

+ Output Data + +
+ +
+

+ + + + + + + {{ ' ' // Search form }} +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + + @if (outputData || true) { +
+ + {{ ' ' // Wrap the all table in a form. This is the only way reactiveforms will work }} +
+ + + + {{ ' ' // Header Row }} + + + + + + + + {{ ' ' // Search Fields Row }} + + + + + + + + + @for (outputData of outputData; track trackId) { + + + + + + + + } + +
+
+ Variable + +
+
+
+ Value + + +
+
+
+ Base Unit + +
+
+ @if (outputData.variable) { + + } + + @if (outputData.variable) { +
+ {{ outputData.variable.name }} +
+ } +
{{ outputData.value }} + @if (outputData.baseUnit) { +
+ {{ outputData.baseUnit.symbol }} +
+ } +
+ +
+ +
+
+ } + +
Loading ...
+ @if (outputData?.length === 0) { +
+ Nenhum Output Data encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/output-data/list/output-data.component.spec.ts b/src/main/webapp/app/entities/output-data/list/output-data.component.spec.ts new file mode 100644 index 0000000..56f6e77 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/list/output-data.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../output-data.test-samples'; +import { OutputDataService } from '../service/output-data.service'; + +import { OutputDataComponent } from './output-data.component'; +import SpyInstance = jest.SpyInstance; + +describe('OutputData Management Component', () => { + let comp: OutputDataComponent; + let fixture: ComponentFixture; + let service: OutputDataService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, OutputDataComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(OutputDataComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(OutputDataComponent); + comp = fixture.componentInstance; + service = TestBed.inject(OutputDataService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.outputData?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to outputDataService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getOutputDataIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getOutputDataIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/output-data/list/output-data.component.ts b/src/main/webapp/app/entities/output-data/list/output-data.component.ts new file mode 100644 index 0000000..8d15bec --- /dev/null +++ b/src/main/webapp/app/entities/output-data/list/output-data.component.ts @@ -0,0 +1,305 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, map } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; + +import { IPeriod } from 'app/entities/period/period.model'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { PeriodVersionService } from 'app/entities/period-version/service/period-version.service'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { IOutputData } from '../output-data.model'; +import { EntityArrayResponseType, OutputDataService } from '../service/output-data.service'; +import { OutputDataSearchFormService, OutputDataSearchFormGroup } from './output-data-search-form.service'; +import { ResilientEnvironmentService } from 'app/resilient/resilient-environment/service/resilient-environment.service'; + +@Component({ + standalone: true, + selector: 'jhi-output-data', + templateUrl: './output-data.component.html', + styleUrl: './output-data.component.css', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + MatProgressSpinnerModule, + ], +}) +export class OutputDataComponent implements OnInit { + subscriptionWorkOrganization!: Subscription; // Store the subscription + selectedOrganization?: IOrganization; + subscription: Subscription | null = null; + outputData?: IOutputData[]; + + isLoading = false; + isExporting: boolean = false; + + sortState = sortStateSignal({}); + + scopesSharedCollection: IVariableScope[] = []; + categoriesSharedCollection: IVariableCategory[] = []; + periodsSharedCollection: IPeriod[] = []; + periodVersionsSharedCollection: IPeriodVersion[] = []; + orgsSharedCollection: IOrganization[] = []; + + public router = inject(Router); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + protected periodService = inject(PeriodService); + protected periodVersionService = inject(PeriodVersionService); + protected organizationService = inject(OrganizationService); + protected outputDataService = inject(OutputDataService); + protected outputDataSearchFormService = inject(OutputDataSearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + protected envService = inject(ResilientEnvironmentService); + + searchForm: OutputDataSearchFormGroup = this.outputDataSearchFormService.createOutputDataSearchFormGroup(); + trackId = (_index: number, item: IOutputData): number => this.outputDataService.getEntityIdentifier(item); + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => + this.periodService.compareEntity(o1, o2); + + comparePeriodVersion = (o1: IPeriodVersion | null, o2: IPeriodVersion | null): boolean => + this.periodVersionService.compareEntity(o1, o2); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + compareScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + compareCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + ngOnInit(): void { + // Get the currently selected ORGANIZATION, by ResilientEnvironmentService + this.subscriptionWorkOrganization = this + .envService + .selectedWorkOrganization + /* .pipe(first()) // READS the Observable only once. Doesn't react to later changes on the subscription */ + .subscribe(org => { + if(org){ + this.selectedOrganization = org; + + // Set filter + this.searchForm.get('ownerId')?.setValue(org.id); + + // Clear list + this.outputData = undefined; + + // Execute search + if (this.searchForm.valid) { + this.load(); + } + } + }); + + // Listen to changes to the scope filter property + this.searchForm.get('variableScopeId')?.valueChanges.subscribe((variableScopeId) => { + this.loadCategoriesOfSelectedScope(variableScopeId); + this.searchForm.get('variableCategoryId')?.setValue(null); + }); + + // Listen to changes to the period filter property + this.searchForm.get('periodId')?.valueChanges.subscribe((periodId) => { + this.loadPeriodVersionsOfSelectedPeriod(periodId); + this.searchForm.get('periodVersionId')?.setValue(null); + }); + + this.loadRelationshipsOptions().subscribe(() => { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.outputData || this.outputData.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + }); + } + + onSearch(): void { + this.load(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + /* this.outputData = this.refineData(dataFromBody); */ + this.outputData = dataFromBody; + + // Allow Angular to process UI updates before turning off loading + this.ngZone.runOutsideAngular(() => { + setTimeout(() => { + this.ngZone.run(() => { + this.isLoading = false; + }); + }, 0); + }); + } + + protected refineData(data: IOutputData[]): IOutputData[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IOutputData[] | null): IOutputData[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + + const searchModel = this.outputDataSearchFormService.getOutputData(this.searchForm); + this.outputDataSearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + + return this.outputDataService.queryByCriteria(queryObject) + .pipe( + tap(() => { + // this.dataLoaded = true; + }) + ); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } + + onPeriodChange(event: Event) { + const selectedValue = (event.target as HTMLSelectElement).value; + this.searchForm.get('periodId')?.setValue(Number(selectedValue)); + } + + protected loadRelationshipsOptions(): Observable { + return new Observable((observer) => { + this.periodService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((periods: IPeriod[]) => { + this.periodsSharedCollection = periods; + + // Set first Period as default. It's mandatory to select a Period + const firstPeriodId = this.periodsSharedCollection[0]?.id; + this.searchForm.get('periodId')?.setValue(firstPeriodId); + + //Load the PeriodVersion list + this.loadPeriodVersionsOfSelectedPeriod(firstPeriodId); + + observer.next(); // Notify that process is complete + observer.complete(); + }); + + this.variableScopeService + .queryForData() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((scopes: IVariableScope[]) => (this.scopesSharedCollection = scopes)); + + + this.organizationService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((orgs: IOrganization[]) => (this.orgsSharedCollection = orgs)); + + }); + } + + protected loadCategoriesOfSelectedScope(variableScopeId: number|null|undefined): void { + if (variableScopeId) { + this.variableCategoryService + .queryByScope(variableScopeId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((cats: IVariableCategory[]) => (this.categoriesSharedCollection = cats)); + } else { + this.categoriesSharedCollection = []; + } + } + + protected loadPeriodVersionsOfSelectedPeriod(periodId: number|null|undefined): void { + if (periodId) { + this.periodService + .queryVersions(periodId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((versions: IPeriodVersion[]) => { + this.periodVersionsSharedCollection = versions; + + // Set first PeriodVersion as default. It's mandatory to select a PeriodVersion + const firstPeriodVersionId = this.periodVersionsSharedCollection[0]?.id; + this.searchForm.get('periodVersionId')?.setValue(firstPeriodVersionId); + }); + } else { + this.periodVersionsSharedCollection = []; + } + } + + export(): void { + this.isExporting = true; + const periodVersionId = this.searchForm.get('periodVersionId')?.value; + + if (periodVersionId) { + this.outputDataService.export(periodVersionId).subscribe((blob: Blob) => { + const a = document.createElement('a'); + const objectUrl = URL.createObjectURL(blob); + a.href = objectUrl; + a.download = 'report.xlsx'; + a.click(); + URL.revokeObjectURL(objectUrl); + + this.isExporting = false; + }); + } else { + this.isExporting = false; + } + } +} diff --git a/src/main/webapp/app/entities/output-data/output-data.model.ts b/src/main/webapp/app/entities/output-data/output-data.model.ts new file mode 100644 index 0000000..2938494 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/output-data.model.ts @@ -0,0 +1,35 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariable } from 'app/entities/variable/variable.model'; +import { IPeriod } from 'app/entities/period/period.model'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { IUnit } from 'app/entities/unit/unit.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; + +export interface IOutputData extends IResilientEntity { + value?: number | null; + variable?: Pick | null; + period?: Pick | null; + periodVersion?: Pick | null; + baseUnit?: Pick | null; + owner?: Pick | null; +} + +export type NewOutputData = Omit & { id: null }; + +export type SearchOutputData = Omit< + IOutputData, + // Remove unwanted properties for search + 'id' | 'variable' | 'period' | 'periodVersion' | 'baseUnit' | 'owner'> + & + // Add specific properties for search + { + id?: number | null; + variableId?: number | null; + variableNameOrCode?: string | null; + periodId?: number | null; + periodVersionId?: number | null; + ownerId?: number | null; + variableScopeId?: number | null; + variableCategoryId?: number | null; + }; diff --git a/src/main/webapp/app/entities/output-data/output-data.routes.ts b/src/main/webapp/app/entities/output-data/output-data.routes.ts new file mode 100644 index 0000000..d48c5da --- /dev/null +++ b/src/main/webapp/app/entities/output-data/output-data.routes.ts @@ -0,0 +1,41 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { OutputDataComponent } from './list/output-data.component'; +import { OutputDataDetailComponent } from './detail/output-data-detail.component'; +import OutputDataResolve from './route/output-data-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { OutputDataService } from './service/output-data.service'; +import { Resources } from 'app/security/resources.model'; + +const outputDataRoute: Routes = [ + { + path: '', + component: OutputDataComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: OutputDataService, + resource: Resources.OUTPUT_DATA, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, + { + path: ':id/view', + component: OutputDataDetailComponent, + resolve: { + outputData: OutputDataResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: OutputDataService, + resource: Resources.OUTPUT_DATA, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR'], + } + }, +]; + +export default outputDataRoute; diff --git a/src/main/webapp/app/entities/output-data/output-data.test-samples.ts b/src/main/webapp/app/entities/output-data/output-data.test-samples.ts new file mode 100644 index 0000000..445209b --- /dev/null +++ b/src/main/webapp/app/entities/output-data/output-data.test-samples.ts @@ -0,0 +1,27 @@ +import { IOutputData, NewOutputData } from './output-data.model'; + +export const sampleWithRequiredData: IOutputData = { + id: 11815, + value: 16616.09, +}; + +export const sampleWithPartialData: IOutputData = { + id: 11969, + value: 23866.55, +}; + +export const sampleWithFullData: IOutputData = { + id: 17858, + value: 3212.39, + version: 9972, +}; + +export const sampleWithNewData: NewOutputData = { + value: 16701.2, + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/output-data/route/output-data-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/output-data/route/output-data-routing-resolve.service.spec.ts new file mode 100644 index 0000000..1ea8132 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/route/output-data-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IOutputData } from '../output-data.model'; +import { OutputDataService } from '../service/output-data.service'; + +import outputDataResolve from './output-data-routing-resolve.service'; + +describe('OutputData routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: OutputDataService; + let resultOutputData: IOutputData | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(OutputDataService); + resultOutputData = undefined; + }); + + describe('resolve', () => { + it('should return IOutputData returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + outputDataResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOutputData = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultOutputData).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + outputDataResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOutputData = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultOutputData).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + outputDataResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultOutputData = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultOutputData).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/output-data/route/output-data-routing-resolve.service.ts b/src/main/webapp/app/entities/output-data/route/output-data-routing-resolve.service.ts new file mode 100644 index 0000000..f57fff6 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/route/output-data-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IOutputData } from '../output-data.model'; +import { OutputDataService } from '../service/output-data.service'; + +const outputDataResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(OutputDataService) + .find(id) + .pipe( + mergeMap((outputData: HttpResponse) => { + if (outputData.body) { + return of(outputData.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default outputDataResolve; diff --git a/src/main/webapp/app/entities/output-data/service/output-data.service.spec.ts b/src/main/webapp/app/entities/output-data/service/output-data.service.spec.ts new file mode 100644 index 0000000..df0716d --- /dev/null +++ b/src/main/webapp/app/entities/output-data/service/output-data.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IOutputData } from '../output-data.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../output-data.test-samples'; + +import { OutputDataService } from './output-data.service'; + +const requireRestSample: IOutputData = { + ...sampleWithRequiredData, +}; + +describe('OutputData Service', () => { + let service: OutputDataService; + let httpMock: HttpTestingController; + let expectedResult: IOutputData | IOutputData[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(OutputDataService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a OutputData', () => { + const outputData = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(outputData).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a OutputData', () => { + const outputData = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(outputData).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a OutputData', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of OutputData', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a OutputData', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addOutputDataToCollectionIfMissing', () => { + it('should add a OutputData to an empty array', () => { + const outputData: IOutputData = sampleWithRequiredData; + expectedResult = service.addOutputDataToCollectionIfMissing([], outputData); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(outputData); + }); + + it('should not add a OutputData to an array that contains it', () => { + const outputData: IOutputData = sampleWithRequiredData; + const outputDataCollection: IOutputData[] = [ + { + ...outputData, + }, + sampleWithPartialData, + ]; + expectedResult = service.addOutputDataToCollectionIfMissing(outputDataCollection, outputData); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a OutputData to an array that doesn't contain it", () => { + const outputData: IOutputData = sampleWithRequiredData; + const outputDataCollection: IOutputData[] = [sampleWithPartialData]; + expectedResult = service.addOutputDataToCollectionIfMissing(outputDataCollection, outputData); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(outputData); + }); + + it('should add only unique OutputData to an array', () => { + const outputDataArray: IOutputData[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const outputDataCollection: IOutputData[] = [sampleWithRequiredData]; + expectedResult = service.addOutputDataToCollectionIfMissing(outputDataCollection, ...outputDataArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const outputData: IOutputData = sampleWithRequiredData; + const outputData2: IOutputData = sampleWithPartialData; + expectedResult = service.addOutputDataToCollectionIfMissing([], outputData, outputData2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(outputData); + expect(expectedResult).toContain(outputData2); + }); + + it('should accept null and undefined values', () => { + const outputData: IOutputData = sampleWithRequiredData; + expectedResult = service.addOutputDataToCollectionIfMissing([], null, outputData, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(outputData); + }); + + it('should return initial array if no OutputData is added', () => { + const outputDataCollection: IOutputData[] = [sampleWithRequiredData]; + expectedResult = service.addOutputDataToCollectionIfMissing(outputDataCollection, undefined, null); + expect(expectedResult).toEqual(outputDataCollection); + }); + }); + + describe('compareOutputData', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareOutputData(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareOutputData(entity1, entity2); + const compareResult2 = service.compareOutputData(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareOutputData(entity1, entity2); + const compareResult2 = service.compareOutputData(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareOutputData(entity1, entity2); + const compareResult2 = service.compareOutputData(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/output-data/service/output-data.service.ts b/src/main/webapp/app/entities/output-data/service/output-data.service.ts new file mode 100644 index 0000000..7e9af37 --- /dev/null +++ b/src/main/webapp/app/entities/output-data/service/output-data.service.ts @@ -0,0 +1,27 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IOutputData, NewOutputData } from '../output-data.model'; + +export type PartialUpdateOutputData = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class OutputDataService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/output-data'); + getResourceUrl(): string { return this.resourceUrl; } + + export(periodVersionId: number): Observable { + return this.http.get(`${this.resourceUrl}/export/${periodVersionId}`, { responseType: 'blob' }); + } +} diff --git a/src/main/webapp/app/entities/period-version/period-version.model.ts b/src/main/webapp/app/entities/period-version/period-version.model.ts new file mode 100644 index 0000000..9a16e11 --- /dev/null +++ b/src/main/webapp/app/entities/period-version/period-version.model.ts @@ -0,0 +1,17 @@ +import dayjs from 'dayjs/esm'; +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodVersionStatus } from 'app/entities/enumerations/period-version-status.model'; + +export interface IPeriodVersion extends IResilientEntity { + name?: string | null; + description?: string | null; + periodVersion?: number | null; + state?: keyof typeof PeriodVersionStatus | null; + creationDate?: dayjs.Dayjs | null; + creationUsername?: string | null; + period?: Pick | null; +} + +export type NewPeriodVersion = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/period-version/route/period-version-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/period-version/route/period-version-routing-resolve.service.spec.ts new file mode 100644 index 0000000..1b93e0e --- /dev/null +++ b/src/main/webapp/app/entities/period-version/route/period-version-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IPeriodVersion } from '../period-version.model'; +import { PeriodVersionService } from '../service/period-version.service'; + +import periodVersionResolve from './period-version-routing-resolve.service'; + +describe('PeriodVersion routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: PeriodVersionService; + let resultPeriodVersion: IPeriodVersion | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(PeriodVersionService); + resultPeriodVersion = undefined; + }); + + describe('resolve', () => { + it('should return IPeriodVersion returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + periodVersionResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultPeriodVersion = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultPeriodVersion).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + periodVersionResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultPeriodVersion = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultPeriodVersion).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + periodVersionResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultPeriodVersion = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultPeriodVersion).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/period-version/route/period-version-routing-resolve.service.ts b/src/main/webapp/app/entities/period-version/route/period-version-routing-resolve.service.ts new file mode 100644 index 0000000..d6e5c88 --- /dev/null +++ b/src/main/webapp/app/entities/period-version/route/period-version-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IPeriodVersion } from '../period-version.model'; +import { PeriodVersionService } from '../service/period-version.service'; + +const periodVersionResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(PeriodVersionService) + .find(id) + .pipe( + mergeMap((periodVersion: HttpResponse) => { + if (periodVersion.body) { + return of(periodVersion.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default periodVersionResolve; diff --git a/src/main/webapp/app/entities/period-version/service/period-version.service.spec.ts b/src/main/webapp/app/entities/period-version/service/period-version.service.spec.ts new file mode 100644 index 0000000..101e389 --- /dev/null +++ b/src/main/webapp/app/entities/period-version/service/period-version.service.spec.ts @@ -0,0 +1,205 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IPeriodVersion } from '../period-version.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../period-version.test-samples'; + +import { PeriodVersionService, RestPeriodVersion } from './period-version.service'; + +const requireRestSample: RestPeriodVersion = { + ...sampleWithRequiredData, + creationDate: sampleWithRequiredData.creationDate?.toJSON(), +}; + +describe('PeriodVersion Service', () => { + let service: PeriodVersionService; + let httpMock: HttpTestingController; + let expectedResult: IPeriodVersion | IPeriodVersion[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(PeriodVersionService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a PeriodVersion', () => { + const periodVersion = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(periodVersion).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a PeriodVersion', () => { + const periodVersion = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(periodVersion).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a PeriodVersion', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of PeriodVersion', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a PeriodVersion', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addPeriodVersionToCollectionIfMissing', () => { + it('should add a PeriodVersion to an empty array', () => { + const periodVersion: IPeriodVersion = sampleWithRequiredData; + expectedResult = service.addPeriodVersionToCollectionIfMissing([], periodVersion); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(periodVersion); + }); + + it('should not add a PeriodVersion to an array that contains it', () => { + const periodVersion: IPeriodVersion = sampleWithRequiredData; + const periodVersionCollection: IPeriodVersion[] = [ + { + ...periodVersion, + }, + sampleWithPartialData, + ]; + expectedResult = service.addPeriodVersionToCollectionIfMissing(periodVersionCollection, periodVersion); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a PeriodVersion to an array that doesn't contain it", () => { + const periodVersion: IPeriodVersion = sampleWithRequiredData; + const periodVersionCollection: IPeriodVersion[] = [sampleWithPartialData]; + expectedResult = service.addPeriodVersionToCollectionIfMissing(periodVersionCollection, periodVersion); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(periodVersion); + }); + + it('should add only unique PeriodVersion to an array', () => { + const periodVersionArray: IPeriodVersion[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const periodVersionCollection: IPeriodVersion[] = [sampleWithRequiredData]; + expectedResult = service.addPeriodVersionToCollectionIfMissing(periodVersionCollection, ...periodVersionArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const periodVersion: IPeriodVersion = sampleWithRequiredData; + const periodVersion2: IPeriodVersion = sampleWithPartialData; + expectedResult = service.addPeriodVersionToCollectionIfMissing([], periodVersion, periodVersion2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(periodVersion); + expect(expectedResult).toContain(periodVersion2); + }); + + it('should accept null and undefined values', () => { + const periodVersion: IPeriodVersion = sampleWithRequiredData; + expectedResult = service.addPeriodVersionToCollectionIfMissing([], null, periodVersion, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(periodVersion); + }); + + it('should return initial array if no PeriodVersion is added', () => { + const periodVersionCollection: IPeriodVersion[] = [sampleWithRequiredData]; + expectedResult = service.addPeriodVersionToCollectionIfMissing(periodVersionCollection, undefined, null); + expect(expectedResult).toEqual(periodVersionCollection); + }); + }); + + describe('comparePeriodVersion', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.comparePeriodVersion(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.comparePeriodVersion(entity1, entity2); + const compareResult2 = service.comparePeriodVersion(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.comparePeriodVersion(entity1, entity2); + const compareResult2 = service.comparePeriodVersion(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.comparePeriodVersion(entity1, entity2); + const compareResult2 = service.comparePeriodVersion(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/period-version/service/period-version.service.ts b/src/main/webapp/app/entities/period-version/service/period-version.service.ts new file mode 100644 index 0000000..7a7f249 --- /dev/null +++ b/src/main/webapp/app/entities/period-version/service/period-version.service.ts @@ -0,0 +1,36 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; + +import dayjs from 'dayjs/esm'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IPeriodVersion, NewPeriodVersion } from '../period-version.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class PeriodVersionService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/period-versions'); + override getResourceUrl(): string { return this.resourceUrl; } + + private readonly dateProperties: Map = new Map([ + ["creationDate", DateTypeEnum.TIMESTAMP] + ]); + override getDateProperties(): Map { return this.dateProperties; } + + /* PeriodVersionService - Service methods */ + /* ************************************** */ + queryByPeriod(periodId: number): Observable { + return this.http.get(`${this.resourceUrl}/byPeriod/${periodId}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.html b/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.html new file mode 100644 index 0000000..5bf25f6 --- /dev/null +++ b/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (period) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.spec.ts new file mode 100644 index 0000000..e641e79 --- /dev/null +++ b/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { PeriodService } from '../service/period.service'; + +import { PeriodDeleteDialogComponent } from './period-delete-dialog.component'; + +describe('Period Management Delete Component', () => { + let comp: PeriodDeleteDialogComponent; + let fixture: ComponentFixture; + let service: PeriodService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PeriodDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(PeriodDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(PeriodDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(PeriodService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.ts b/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.ts new file mode 100644 index 0000000..537473f --- /dev/null +++ b/src/main/webapp/app/entities/period/delete/period-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IPeriod } from '../period.model'; +import { PeriodService } from '../service/period.service'; + +@Component({ + standalone: true, + templateUrl: './period-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class PeriodDeleteDialogComponent { + period?: IPeriod; + + protected periodService = inject(PeriodService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.periodService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/period/detail/period-detail.component.html b/src/main/webapp/app/entities/period/detail/period-detail.component.html new file mode 100644 index 0000000..cd82eb9 --- /dev/null +++ b/src/main/webapp/app/entities/period/detail/period-detail.component.html @@ -0,0 +1,127 @@ + + +
+
Name
+
+ {{ period()!.name }} +
+
+ Description +
+
+ {{ period()!.description }} +
+
+ Begin Date +
+
+ {{ period()!.beginDate | formatMediumDate }} +
+
+ End Date +
+
+ {{ period()!.endDate | formatMediumDate }} +
+
State
+
+ {{ + { null: '', CREATED: 'CREATED',OPENED: 'OPENED', CLOSED: 'CLOSED', PROCESSING: 'PROCESSING', ENDED: 'ENDED' }[period()!.state ?? 'null'] + }} +
+ +
+ + @if(period()!.periodVersions!.length>0) { + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
NameDescriptionPeriod VersionState
{{i+1}} + {{ periodVersion!.name }} + + {{ periodVersion!.description }} + + {{ periodVersion!.periodVersion }} + + {{ + { null: '', PROCESSING: 'PROCESSING', ENDED: 'ENDED', ERROR: 'ERROR' }[periodVersion!.state ?? 'null'] + }} + + + +
+
+
+ } + + + + + + + + + + +
\ No newline at end of file diff --git a/src/main/webapp/app/entities/period/detail/period-detail.component.spec.ts b/src/main/webapp/app/entities/period/detail/period-detail.component.spec.ts new file mode 100644 index 0000000..60ae6a5 --- /dev/null +++ b/src/main/webapp/app/entities/period/detail/period-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { PeriodDetailComponent } from './period-detail.component'; + +describe('Period Management Detail Component', () => { + let comp: PeriodDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PeriodDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: PeriodDetailComponent, + resolve: { period: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(PeriodDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PeriodDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load period on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', PeriodDetailComponent); + + // THEN + expect(instance.period()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/period/detail/period-detail.component.ts b/src/main/webapp/app/entities/period/detail/period-detail.component.ts new file mode 100644 index 0000000..2e874e0 --- /dev/null +++ b/src/main/webapp/app/entities/period/detail/period-detail.component.ts @@ -0,0 +1,124 @@ +import { Component, input, inject, OnInit, signal } from '@angular/core'; +import { RouterModule, Router, ActivatedRoute } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { finalize, map } from 'rxjs/operators'; +import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IPeriod, NewPeriod } from '../period.model'; +import { PeriodStatus } from 'app/entities/enumerations/period-status.model'; +import { IActivityInfo } from 'app/resilient/resilient-activity/resilient-activity.model'; +import { ResilientActivityService } from 'app/resilient/resilient-activity/service/resilient-activity.service'; +import { ResilientActivityActionsButtonComponent } from 'app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component'; +import { ResilientActivityProgressComponent } from 'app/resilient/resilient-activity/progress/resilient-activity-progress.component'; +import { PeriodService } from '../service/period.service'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +import { BaseDetailLayoutComponent } from 'app/layouts/base-detail/base-detail.component' +import { ResilientLogLogsDialogComponent } from 'app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-period-detail', + templateUrl: './period-detail.component.html', + imports: [ + SharedModule, + RouterModule, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + NgbDropdownModule, + ResilientActivityActionsButtonComponent, + ResilientActivityProgressComponent, + BaseDetailLayoutComponent + ], +}) +export class PeriodDetailComponent extends AbstractResilientEntityComponent implements OnInit { + period = input(null); + currentTab: string = "versions"; + + activityCollection: IActivityInfo[] = []; + public PeriodStatus = PeriodStatus; + + protected resilientActivityService = inject(ResilientActivityService); + protected router = inject(Router); + protected route = inject(ActivatedRoute); + protected periodService = inject(PeriodService); + protected modalService = inject(NgbModal); + + constructor( + service: PeriodService + ) { super(service); } + + ngOnInit(): void { + this.loadActivitiesForActivityDomain(); + } + + previousState(): void { + window.history.back(); + } + + setTab(tab: string): void { + this.currentTab = tab; + } + + handleActivityInvoked(event: string): void { + this.reloadPage(); + } + + protected loadActivitiesForActivityDomain(): void { + this.resilientActivityService + .activityInfo("com.oguerreiro.resilient.domain.Period", PeriodStatus[this.period()?.state!]) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((activities: IActivityInfo[]) => (this.activityCollection = activities)); + } + + /* Refresh the current record. Probably something was invoked in the server and the record mus be updated */ + reloadPage(): void { + this.refreshPage(); + } + + refreshPage(): void { + // Get the current entity ID from the route + const entityId = this.route.snapshot.params['id']; + + // Navigate to the same detail page with the same ID + const currentUrl: string = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + // this.router.navigate(['/period', entityId, 'view']); + this.router.navigateByUrl(currentUrl); + }); + } + + reloadDomain(): void { + let id: number; + + if (!this.period()?.id) + return; + id = this.period()!.id; + + this.periodService + .find(id) + /* .pipe(map((res: HttpResponse) => res.body ?? [])) */ + .subscribe((res: HttpResponse) => { + const period: IPeriod | null = res.body ?? null; + // this.period = of(period); + }); + } + + /** + * To open in a new window, must be done using window.open(). The angular [routerLink] (in .html) always opens in the same browser window + */ + openDashboard(periodId: number, periodVersionId: number): void { + const url = this.router.serializeUrl( + this.router.createUrlTree(['/dashboard', 'EMISSIONS', periodId, periodVersionId]) + ); + window.open(url, '_blank'); + } + + showLogs(): void { + const modalRef = this.modalService.open(ResilientLogLogsDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.ownerId = this.period()?.id; + } +} diff --git a/src/main/webapp/app/entities/period/list/period.component.html b/src/main/webapp/app/entities/period/list/period.component.html new file mode 100644 index 0000000..c8f8dfd --- /dev/null +++ b/src/main/webapp/app/entities/period/list/period.component.html @@ -0,0 +1,113 @@ + +
+

+ Periods + +
+ + + +
+

+ + + + + + @if (periods?.length === 0) { +
+ Nenhum Periods encontrado +
+ } + + @if (periods && periods.length > 0) { +
+ + + + + + + + + + + + + + @for (period of periods; track trackId) { + + + + + + + + + + } + +
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Begin Date + + +
+
+
+ End Date + + +
+
+
+ State + + +
+
{{ period.name }}{{ period.description }}{{ period.beginDate | formatMediumDate }}{{ period.endDate | formatMediumDate }} + {{ { null: '', CREATED: 'CREATED',OPENED: 'OPENED', CLOSED: 'CLOSED', PROCESSING: 'PROCESSING', ENDED: 'ENDED' }[period.state ?? 'null'] }} + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/period/list/period.component.html.old b/src/main/webapp/app/entities/period/list/period.component.html.old new file mode 100644 index 0000000..8fa8b6a --- /dev/null +++ b/src/main/webapp/app/entities/period/list/period.component.html.old @@ -0,0 +1,144 @@ +
+

+ Periods + +
+ + + +
+

+ + + + + + @if (periods?.length === 0) { +
+ Nenhum Periods encontrado +
+ } + + @if (periods && periods.length > 0) { +
+ + + + + + + + + + + + + + + + + @for (period of periods; track trackId) { + + + + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Begin Date + + +
+
+
+ End Date + + +
+
+
+ State + + +
+
+
+ Creation Date + + +
+
+
+ Creation Username + + +
+
+
+ Version + + +
+
+ {{ period.id }} + {{ period.name }}{{ period.description }}{{ period.beginDate | formatMediumDate }}{{ period.endDate | formatMediumDate }} + {{ { null: '', OPEN: 'OPEN', REOPEN: 'REOPEN', CLOSED: 'CLOSED' }[period.state ?? 'null'] }} + {{ period.creationDate | formatMediumDatetime }}{{ period.creationUsername }}{{ period.version }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/period/list/period.component.spec.ts b/src/main/webapp/app/entities/period/list/period.component.spec.ts new file mode 100644 index 0000000..403885f --- /dev/null +++ b/src/main/webapp/app/entities/period/list/period.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../period.test-samples'; +import { PeriodService } from '../service/period.service'; + +import { PeriodComponent } from './period.component'; +import SpyInstance = jest.SpyInstance; + +describe('Period Management Component', () => { + let comp: PeriodComponent; + let fixture: ComponentFixture; + let service: PeriodService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PeriodComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(PeriodComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(PeriodComponent); + comp = fixture.componentInstance; + service = TestBed.inject(PeriodService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.periods?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to periodService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getPeriodIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getPeriodIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/period/list/period.component.ts b/src/main/webapp/app/entities/period/list/period.component.ts new file mode 100644 index 0000000..54984d4 --- /dev/null +++ b/src/main/webapp/app/entities/period/list/period.component.ts @@ -0,0 +1,124 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IPeriod, NewPeriod } from '../period.model'; +import { EntityArrayResponseType, PeriodService } from '../service/period.service'; +import { PeriodDeleteDialogComponent } from '../delete/period-delete-dialog.component'; +import { AbstractResilientEntityComponent } from 'app/entities/resilient/resilient-entity.component'; + +@Component({ + standalone: true, + selector: 'jhi-period', + templateUrl: './period.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class PeriodComponent extends AbstractResilientEntityComponent implements OnInit { + subscription: Subscription | null = null; + periods?: IPeriod[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected periodService = inject(PeriodService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IPeriod): number => this.periodService.getEntityIdentifier(item); + + constructor(service: PeriodService) { super(service); } + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.periods || this.periods.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(period: IPeriod): void { + const modalRef = this.modalService.open(PeriodDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.period = period; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.periods = this.refineData(dataFromBody); + } + + protected refineData(data: IPeriod[]): IPeriod[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IPeriod[] | null): IPeriod[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.periodService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/period/period.model.ts b/src/main/webapp/app/entities/period/period.model.ts new file mode 100644 index 0000000..36284e4 --- /dev/null +++ b/src/main/webapp/app/entities/period/period.model.ts @@ -0,0 +1,19 @@ +import dayjs from 'dayjs/esm'; +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { PeriodStatus } from 'app/entities/enumerations/period-status.model'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { IActivityDomain } from 'app/entities/activity/activity.model'; + +export interface IPeriod extends IActivityDomain { + name?: string | null; + description?: string | null; + beginDate?: dayjs.Dayjs | null; + endDate?: dayjs.Dayjs | null; + state?: keyof typeof PeriodStatus | null; + creationDate?: dayjs.Dayjs | null; + creationUsername?: string | null; + periodVersions?: IPeriodVersion[] | null; +} + +export type NewPeriod = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/period/period.routes.ts b/src/main/webapp/app/entities/period/period.routes.ts new file mode 100644 index 0000000..ac6a4d5 --- /dev/null +++ b/src/main/webapp/app/entities/period/period.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { PeriodComponent } from './list/period.component'; +import { PeriodDetailComponent } from './detail/period-detail.component'; +import { PeriodUpdateComponent } from './update/period-update.component'; +import PeriodResolve from './route/period-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { PeriodService } from './service/period.service'; +import { Resources } from 'app/security/resources.model'; + +const periodRoute: Routes = [ + { + path: '', + component: PeriodComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: PeriodService, + resource: Resources.PERIOD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: PeriodDetailComponent, + resolve: { + period: PeriodResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: PeriodService, + resource: Resources.PERIOD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: PeriodUpdateComponent, + resolve: { + period: PeriodResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: PeriodService, + resource: Resources.PERIOD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: PeriodUpdateComponent, + resolve: { + period: PeriodResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: PeriodService, + resource: Resources.PERIOD, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default periodRoute; diff --git a/src/main/webapp/app/entities/period/period.test-samples.ts b/src/main/webapp/app/entities/period/period.test-samples.ts new file mode 100644 index 0000000..9f5bc01 --- /dev/null +++ b/src/main/webapp/app/entities/period/period.test-samples.ts @@ -0,0 +1,50 @@ +import dayjs from 'dayjs/esm'; + +import { IPeriod, NewPeriod } from './period.model'; + +export const sampleWithRequiredData: IPeriod = { + id: 28671, + name: 'likely dull modulo', + beginDate: dayjs('2024-10-14'), + endDate: dayjs('2024-10-15'), + state: 'REOPEN', + creationDate: dayjs('2024-10-14T10:52'), + creationUsername: 'when pack barring', +}; + +export const sampleWithPartialData: IPeriod = { + id: 6405, + name: 'ouch a', + beginDate: dayjs('2024-10-14'), + endDate: dayjs('2024-10-14'), + state: 'OPEN', + creationDate: dayjs('2024-10-14T14:00'), + creationUsername: 'quickly conciliate anxiously', +}; + +export const sampleWithFullData: IPeriod = { + id: 2883, + name: 'phew', + description: 'sparse cramp', + beginDate: dayjs('2024-10-15'), + endDate: dayjs('2024-10-14'), + state: 'OPEN', + creationDate: dayjs('2024-10-14T22:19'), + creationUsername: 'hence maunder between', + version: 31356, +}; + +export const sampleWithNewData: NewPeriod = { + name: 'athwart', + beginDate: dayjs('2024-10-14'), + endDate: dayjs('2024-10-14'), + state: 'REOPEN', + creationDate: dayjs('2024-10-14T23:19'), + creationUsername: 'mmm swiftly overlook', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/period/route/period-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/period/route/period-routing-resolve.service.spec.ts new file mode 100644 index 0000000..6dd0e82 --- /dev/null +++ b/src/main/webapp/app/entities/period/route/period-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IPeriod } from '../period.model'; +import { PeriodService } from '../service/period.service'; + +import periodResolve from './period-routing-resolve.service'; + +describe('Period routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: PeriodService; + let resultPeriod: IPeriod | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(PeriodService); + resultPeriod = undefined; + }); + + describe('resolve', () => { + it('should return IPeriod returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + periodResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultPeriod = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultPeriod).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + periodResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultPeriod = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultPeriod).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + periodResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultPeriod = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultPeriod).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/period/route/period-routing-resolve.service.ts b/src/main/webapp/app/entities/period/route/period-routing-resolve.service.ts new file mode 100644 index 0000000..a955340 --- /dev/null +++ b/src/main/webapp/app/entities/period/route/period-routing-resolve.service.ts @@ -0,0 +1,45 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { IPeriod } from '../period.model'; +import { PeriodService } from '../service/period.service'; + +const periodResolve = (route: ActivatedRouteSnapshot): Observable => { + const router = inject(Router); + const translate = inject(TranslateService); + const periodService = inject(PeriodService); + const id = route.params['id']; + if (id) { + return inject(PeriodService) + .find(id) + .pipe( + mergeMap((period: HttpResponse) => { + if (period.body) { + const periodDomain = period.body; + + // Check if is /edit route, and the state of the InputDataUpload + const path = route.routeConfig?.path; + if (path && path.endsWith('/edit') && !periodService.canUpdate(periodDomain)) { + router.navigate(['/period', id, 'view'], { + state: { message: translate.instant('resilientApp.inputDataUpload.home.editNotAllowedForState') } + }); + return EMPTY; + } else { + return of(period.body); + } + + } else { + router.navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default periodResolve; diff --git a/src/main/webapp/app/entities/period/service/period.service.spec.ts b/src/main/webapp/app/entities/period/service/period.service.spec.ts new file mode 100644 index 0000000..341139d --- /dev/null +++ b/src/main/webapp/app/entities/period/service/period.service.spec.ts @@ -0,0 +1,208 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { DATE_FORMAT } from 'app/config/input.constants'; +import { IPeriod } from '../period.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../period.test-samples'; + +import { PeriodService, RestPeriod } from './period.service'; + +const requireRestSample: RestPeriod = { + ...sampleWithRequiredData, + beginDate: sampleWithRequiredData.beginDate?.format(DATE_FORMAT), + endDate: sampleWithRequiredData.endDate?.format(DATE_FORMAT), + creationDate: sampleWithRequiredData.creationDate?.toJSON(), +}; + +describe('Period Service', () => { + let service: PeriodService; + let httpMock: HttpTestingController; + let expectedResult: IPeriod | IPeriod[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(PeriodService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a Period', () => { + const period = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(period).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a Period', () => { + const period = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(period).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a Period', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of Period', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a Period', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addPeriodToCollectionIfMissing', () => { + it('should add a Period to an empty array', () => { + const period: IPeriod = sampleWithRequiredData; + expectedResult = service.addPeriodToCollectionIfMissing([], period); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(period); + }); + + it('should not add a Period to an array that contains it', () => { + const period: IPeriod = sampleWithRequiredData; + const periodCollection: IPeriod[] = [ + { + ...period, + }, + sampleWithPartialData, + ]; + expectedResult = service.addPeriodToCollectionIfMissing(periodCollection, period); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a Period to an array that doesn't contain it", () => { + const period: IPeriod = sampleWithRequiredData; + const periodCollection: IPeriod[] = [sampleWithPartialData]; + expectedResult = service.addPeriodToCollectionIfMissing(periodCollection, period); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(period); + }); + + it('should add only unique Period to an array', () => { + const periodArray: IPeriod[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const periodCollection: IPeriod[] = [sampleWithRequiredData]; + expectedResult = service.addPeriodToCollectionIfMissing(periodCollection, ...periodArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const period: IPeriod = sampleWithRequiredData; + const period2: IPeriod = sampleWithPartialData; + expectedResult = service.addPeriodToCollectionIfMissing([], period, period2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(period); + expect(expectedResult).toContain(period2); + }); + + it('should accept null and undefined values', () => { + const period: IPeriod = sampleWithRequiredData; + expectedResult = service.addPeriodToCollectionIfMissing([], null, period, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(period); + }); + + it('should return initial array if no Period is added', () => { + const periodCollection: IPeriod[] = [sampleWithRequiredData]; + expectedResult = service.addPeriodToCollectionIfMissing(periodCollection, undefined, null); + expect(expectedResult).toEqual(periodCollection); + }); + }); + + describe('comparePeriod', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.comparePeriod(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.comparePeriod(entity1, entity2); + const compareResult2 = service.comparePeriod(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.comparePeriod(entity1, entity2); + const compareResult2 = service.comparePeriod(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.comparePeriod(entity1, entity2); + const compareResult2 = service.comparePeriod(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/period/service/period.service.ts b/src/main/webapp/app/entities/period/service/period.service.ts new file mode 100644 index 0000000..8e55a77 --- /dev/null +++ b/src/main/webapp/app/entities/period/service/period.service.ts @@ -0,0 +1,84 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IPeriod, NewPeriod } from '../period.model'; +import { IPeriodVersion } from 'app/entities/period-version/period-version.model'; +import { PeriodStatus } from 'app/entities/enumerations/period-status.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class PeriodService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + /* ResilientService - Overriden methods */ + /* ************************************ */ + + override canUpdate(period: IPeriod): boolean { + // Its null!! Allow edition... strange... + if (!period) return true; + + // If activityProgressKey is NOT null, than we can't edit a running ActivityDomain + if (period.activityProgressKey) return false; + + // Only CREATED Period's allow edition + if (period && period.state !== PeriodStatus.CREATED) return false; + + //Otherwise, allow + return true; + } + + override canDelete(period: IPeriod): boolean { + if (!period) return true; + + // If still in CREATE, it can be deleted safely + if (period.state === PeriodStatus.CREATED) return true; + + // If is ENDED, will never be deletable. + if (period.state === PeriodStatus.ENDED) return false; + + // Remaining situations are more complex and require some kind of server request. + // - If contains any PeriodVersion, it's no longer deletable. + // - If Doesn's contains PeriodVersion, in teory it should delete everyting associated and then delete the Period + // TODO: Invoke server to get a more functional response + + return false; // TODO: Review this. For now, its better to prevent DELETE by default + } + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/periods'); + override getResourceUrl(): string { return this.resourceUrl; } + + private readonly dateProperties: Map = new Map([ + ["beginDate", DateTypeEnum.DATE], + ["endDate", DateTypeEnum.DATE], + ["creationDate", DateTypeEnum.TIMESTAMP] + ]); + override getDateProperties(): Map { return this.dateProperties; } + + /* PeriodService - Service methods */ + /* ************************************** */ + queryVersions(periodId: number): Observable> { + return this.http.get(`${this.resourceUrl}/${periodId}/versions`, { observe: 'response' }); + } + + lastVersion(periodId: number): Observable> { + return this.http.get(`${this.resourceUrl}/${periodId}/lastVersion`, { observe: 'response' }); + } + + getVersion(periodVersionId: number): Observable> { + return this.http.get(`${this.resourceUrl}/${periodVersionId}/version`, { observe: 'response' }); + } + + queryByState(state: PeriodStatus): Observable> { + return this.http.get(`${this.resourceUrl}/byState/${state}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/period/update/period-form.service.spec.ts b/src/main/webapp/app/entities/period/update/period-form.service.spec.ts new file mode 100644 index 0000000..b3c76a3 --- /dev/null +++ b/src/main/webapp/app/entities/period/update/period-form.service.spec.ts @@ -0,0 +1,100 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../period.test-samples'; + +import { PeriodFormService } from './period-form.service'; + +describe('Period Form Service', () => { + let service: PeriodFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PeriodFormService); + }); + + describe('Service methods', () => { + describe('createPeriodFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createPeriodFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + beginDate: expect.any(Object), + endDate: expect.any(Object), + state: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + + it('passing IPeriod should create a new form with FormGroup', () => { + const formGroup = service.createPeriodFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + beginDate: expect.any(Object), + endDate: expect.any(Object), + state: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + }); + + describe('getPeriod', () => { + it('should return NewPeriod for default Period initial value', () => { + const formGroup = service.createPeriodFormGroup(sampleWithNewData); + + const period = service.getPeriod(formGroup) as any; + + expect(period).toMatchObject(sampleWithNewData); + }); + + it('should return NewPeriod for empty Period initial value', () => { + const formGroup = service.createPeriodFormGroup(); + + const period = service.getPeriod(formGroup) as any; + + expect(period).toMatchObject({}); + }); + + it('should return IPeriod', () => { + const formGroup = service.createPeriodFormGroup(sampleWithRequiredData); + + const period = service.getPeriod(formGroup) as any; + + expect(period).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IPeriod should not enable id FormControl', () => { + const formGroup = service.createPeriodFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewPeriod should disable id FormControl', () => { + const formGroup = service.createPeriodFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/period/update/period-form.service.ts b/src/main/webapp/app/entities/period/update/period-form.service.ts new file mode 100644 index 0000000..abf51ba --- /dev/null +++ b/src/main/webapp/app/entities/period/update/period-form.service.ts @@ -0,0 +1,123 @@ +import { Injectable, inject } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { IPeriod, NewPeriod } from '../period.model'; + +import { dateRangeValidator, dateLTValidator, dateGTValidator } from '../../resilient/resilient-validators-form.service' + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IPeriod for edit and NewPeriodFormGroupInput for create. + */ +type PeriodFormGroupInput = IPeriod | PartialWithRequiredKeyOf; + +/** + * Type that converts some properties for forms. + */ +type FormValueOf = Omit & { + creationDate?: string | null; +}; + +type PeriodFormRawValue = FormValueOf; + +type NewPeriodFormRawValue = FormValueOf; + +type PeriodFormDefaults = Pick; + +type PeriodFormGroupContent = { + id: FormControl; + name: FormControl; + description: FormControl; + beginDate: FormControl; + endDate: FormControl; + state: FormControl; + creationDate: FormControl; + creationUsername: FormControl; + version: FormControl; +}; + +export type PeriodFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class PeriodFormService { + constructor(private fb: FormBuilder) {} + + createPeriodFormGroup(period: PeriodFormGroupInput = { id: null }): PeriodFormGroup { + const periodRawValue = this.convertPeriodToPeriodRawValue({ + ...this.getFormDefaults(), + ...period, + }); + + const items = period?.periodVersions??[]; + + return new FormGroup({ + id: new FormControl( + { value: periodRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + name: new FormControl(periodRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(periodRawValue.description), + beginDate: new FormControl(periodRawValue.beginDate, { + validators: [Validators.required, dateLTValidator('endDate')], + }), + endDate: new FormControl(periodRawValue.endDate, { + validators: [Validators.required, dateGTValidator('beginDate')], + }), + state: new FormControl(periodRawValue.state), + creationDate: new FormControl(periodRawValue.creationDate), + creationUsername: new FormControl(periodRawValue.creationUsername), + version: new FormControl(periodRawValue.version) + }); + } + + getPeriod(form: PeriodFormGroup): IPeriod | NewPeriod { + return this.convertPeriodRawValueToPeriod(form.getRawValue() as PeriodFormRawValue | NewPeriodFormRawValue); + } + + resetForm(form: PeriodFormGroup, period: PeriodFormGroupInput): void { + const periodRawValue = this.convertPeriodToPeriodRawValue({ ...this.getFormDefaults(), ...period }); + form.reset( + { + ...periodRawValue, + id: { value: periodRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): PeriodFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + creationDate: currentTime, + }; + } + + private convertPeriodRawValueToPeriod(rawPeriod: PeriodFormRawValue | NewPeriodFormRawValue): IPeriod | NewPeriod { + return { + ...rawPeriod, + creationDate: dayjs(rawPeriod.creationDate, DATE_TIME_FORMAT), + }; + } + + private convertPeriodToPeriodRawValue( + period: IPeriod | (Partial & PeriodFormDefaults), + ): PeriodFormRawValue | PartialWithRequiredKeyOf { + return { + ...period, + creationDate: period.creationDate ? period.creationDate.format(DATE_TIME_FORMAT) : undefined, + }; + } +} diff --git a/src/main/webapp/app/entities/period/update/period-update.component.html b/src/main/webapp/app/entities/period/update/period-update.component.html new file mode 100644 index 0000000..7c92702 --- /dev/null +++ b/src/main/webapp/app/entities/period/update/period-update.component.html @@ -0,0 +1,253 @@ + +
+
+
+

+ Criar ou editar Period +

+ +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + +
+
+ +
+ + +
+
+ [dateLessThanInvalid] Field B is required when Field A has a specific value. +
+
+ O campo é obrigatório. +
+ +
+
+ +
+ + +
+
+ [dateGreaterThanInvalid] Field B is required when Field A has a specific value. +
+
+ O campo é obrigatório. +
+ +
+ @if (editForm.controls.id.value !== null) { +
+
+
+ State +
+
+ {{ period!.state }} +
+
+
+ } + + + +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/period/update/period-update.component.spec.ts b/src/main/webapp/app/entities/period/update/period-update.component.spec.ts new file mode 100644 index 0000000..c112786 --- /dev/null +++ b/src/main/webapp/app/entities/period/update/period-update.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { PeriodService } from '../service/period.service'; +import { IPeriod } from '../period.model'; +import { PeriodFormService } from './period-form.service'; + +import { PeriodUpdateComponent } from './period-update.component'; + +describe('Period Management Update Component', () => { + let comp: PeriodUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let periodFormService: PeriodFormService; + let periodService: PeriodService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PeriodUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(PeriodUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(PeriodUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + periodFormService = TestBed.inject(PeriodFormService); + periodService = TestBed.inject(PeriodService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should update editForm', () => { + const period: IPeriod = { id: 456 }; + + activatedRoute.data = of({ period }); + comp.ngOnInit(); + + expect(comp.period).toEqual(period); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const period = { id: 123 }; + jest.spyOn(periodFormService, 'getPeriod').mockReturnValue(period); + jest.spyOn(periodService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ period }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: period })); + saveSubject.complete(); + + // THEN + expect(periodFormService.getPeriod).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(periodService.update).toHaveBeenCalledWith(expect.objectContaining(period)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const period = { id: 123 }; + jest.spyOn(periodFormService, 'getPeriod').mockReturnValue({ id: null }); + jest.spyOn(periodService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ period: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: period })); + saveSubject.complete(); + + // THEN + expect(periodFormService.getPeriod).toHaveBeenCalled(); + expect(periodService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const period = { id: 123 }; + jest.spyOn(periodService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ period }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(periodService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/period/update/period-update.component.ts b/src/main/webapp/app/entities/period/update/period-update.component.ts new file mode 100644 index 0000000..fe29c11 --- /dev/null +++ b/src/main/webapp/app/entities/period/update/period-update.component.ts @@ -0,0 +1,85 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule, FormArray } from '@angular/forms'; + +import { PeriodStatus } from 'app/entities/enumerations/period-status.model'; +import { IPeriod } from '../period.model'; +import { PeriodService } from '../service/period.service'; +import { PeriodFormService, PeriodFormGroup } from './period-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-period-update', + templateUrl: './period-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class PeriodUpdateComponent implements OnInit { + isSaving = false; + period: IPeriod | null = null; + periodStatusValues = Object.keys(PeriodStatus); + currentTab: string = "versions"; + + protected periodService = inject(PeriodService); + protected periodFormService = inject(PeriodFormService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: PeriodFormGroup = this.periodFormService.createPeriodFormGroup(); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ period }) => { + this.period = period; + if (period) { + this.updateForm(period); + } + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const period = this.periodFormService.getPeriod(this.editForm); + if (period.id !== null) { + this.subscribeToSaveResponse(this.periodService.update(period)); + } else { + this.subscribeToSaveResponse(this.periodService.create(period)); + } + } + + /* Tab's navigation */ + setTab(tabName: string): void { + this.currentTab = tabName; + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(period: IPeriod): void { + this.period = period; + this.periodFormService.resetForm(this.editForm, period); + } +} diff --git a/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.css b/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.css new file mode 100644 index 0000000..9f71dc3 --- /dev/null +++ b/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.css @@ -0,0 +1,4 @@ +:host .modal-body { + overflow-y: auto; + max-height: 500px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.html b/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.html new file mode 100644 index 0000000..db3cfb9 --- /dev/null +++ b/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.html @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.ts b/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.ts new file mode 100644 index 0000000..100493b --- /dev/null +++ b/src/main/webapp/app/entities/resilient/log/logs-dialog/resilient-log-logs-dialog.component.ts @@ -0,0 +1,81 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { sortStateSignal, SortDirective, SortByDirective, SortService } from 'app/shared/sort'; +import { Observable, tap } from 'rxjs'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IResilientLog } from '../resilient-log.model'; +import { EntityArrayResponseType, ResilientLogService } from '../service/resilient-log.service'; + +@Component({ + standalone: true, + templateUrl: './resilient-log-logs-dialog.component.html', + styleUrl: './resilient-log-logs-dialog.component.css', + imports: [ + SharedModule, + FormsModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class ResilientLogLogsDialogComponent implements OnInit { + ownerId?: number; + resilientLogs?: IResilientLog[]; + isLoading = false; + + sortState = sortStateSignal({}); + + protected sortService = inject(SortService); + protected resilientLogService = inject(ResilientLogService); + protected activeModal = inject(NgbActiveModal); + + trackId = (_index: number, item: IResilientLog): number => this.resilientLogService.getEntityIdentifier(item); + + setOwnerId(ownerId: number) { + this.ownerId = ownerId; + } + + ngOnInit(): void { + if (this.ownerId) { + this.load(); + } + } + + cancel(): void { + this.activeModal.dismiss(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.resilientLogs = this.refineData(dataFromBody); + } + + protected refineData(data: IResilientLog[]): IResilientLog[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IResilientLog[] | null): IResilientLog[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + + return this.resilientLogService.findByOwner(this.ownerId ?? 0).pipe(tap(() => (this.isLoading = false))); + + } +} diff --git a/src/main/webapp/app/entities/resilient/log/resilient-log.model.ts b/src/main/webapp/app/entities/resilient/log/resilient-log.model.ts new file mode 100644 index 0000000..4bb9b6f --- /dev/null +++ b/src/main/webapp/app/entities/resilient/log/resilient-log.model.ts @@ -0,0 +1,13 @@ +import dayjs from 'dayjs/esm'; +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; +import { ResilientLogLevel } from 'app/entities/enumerations/resilient-log-level.model'; + +export interface IResilientLog extends IResilientEntity { + ownerId?: number | null; + level?: ResilientLogLevel | null; + logMessage?: string | null; + creationDate?: dayjs.Dayjs | null; + creationUsername?: string | null; +} + +export type NewResilientLog = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/resilient/log/service/resilient-log.service.ts b/src/main/webapp/app/entities/resilient/log/service/resilient-log.service.ts new file mode 100644 index 0000000..9dfea8a --- /dev/null +++ b/src/main/webapp/app/entities/resilient/log/service/resilient-log.service.ts @@ -0,0 +1,35 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService, DateTypeEnum, RestOf } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IResilientLog, NewResilientLog } from '../resilient-log.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class ResilientLogService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + /* ResilientService - Service Methods */ + /* ********************************** */ + private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/resilient-logs'); + override getResourceUrl(): string { return this.resourceUrl; } + + private readonly dateProperties: Map = new Map([ + ["creationDate", DateTypeEnum.TIMESTAMP] + ]); + override getDateProperties(): Map { return this.dateProperties; } + + + findByOwner(id: number): Observable { + return this.http + .get[]>(`${this.resourceUrl}/owner/${id}`, { observe: 'response' }) + .pipe(map(res => this.convertResponseArrayFromServer(res))); + } +} diff --git a/src/main/webapp/app/entities/resilient/resilient-entity.component.ts b/src/main/webapp/app/entities/resilient/resilient-entity.component.ts new file mode 100644 index 0000000..c1d547d --- /dev/null +++ b/src/main/webapp/app/entities/resilient/resilient-entity.component.ts @@ -0,0 +1,137 @@ +import { Directive } from '@angular/core'; +import { inject } from '@angular/core'; +import { AccountService } from 'app/core/auth/account.service'; +import { Resources } from 'app/security/resources.model'; +import { SecurityAction } from 'app/security/security-action.model'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; +import { SecurityPermission } from 'app/security/security-permission.model'; +import { ISecurityGroupPermission } from 'app/security/security-group/security-group-permission.model'; + +/** + * Abstract Resilient entity component that provides basic generic functions. + * @param Entity domain interface (Ex. Period) + * @param Entity domain new interface (Ex. NewPeriod) + * @template S - The type of service this component depends on. + */ +@Directive() // Use Directive to mark it as an abstract class; not instantiated directly +export abstract class AbstractResilientEntityComponent> { + private createPermissionCache: SecurityPermission | undefined; + private readPermissionCache: SecurityPermission | undefined; + private updatePermissionCache: SecurityPermission | undefined; + private deletePermissionCache: SecurityPermission | undefined; + + private allowPermissions = [SecurityPermission.ALL, SecurityPermission.HIERARCHY]; + + protected accountService = inject(AccountService); + + + /** + * This makes the enum accessible in the template + * Use it, mainly, in html templates for security. + * Example: + * + */ + Resources = Resources; + + /** + * The constructor for the abstract class. + * @param service - A service instance of type S, used to fetch data. + */ + constructor(protected service: S) { + this.service = service; + } + + /* + * Security Permission methods. + * Evaluates if the current authenticated user has PERMISSION to do the action + * In html template, if security permission fails, the secured element is hidden. + * Usage (delete): + * + * + * Usage (save): + * + */ + getCreatePermission(evalResource: Resources): SecurityPermission { + if (this.createPermissionCache === undefined) { + this.createPermissionCache = this.accountService.getPermission(evalResource, SecurityAction.CREATE); + } + return this.createPermissionCache; + } + + getReadPermission(evalResource: Resources): SecurityPermission { + if (this.readPermissionCache === undefined) { + this.readPermissionCache = this.accountService.getPermission(evalResource, SecurityAction.READ); + } + return this.readPermissionCache; + } + + getUpdatePermission(evalResource: Resources): SecurityPermission { + if (this.updatePermissionCache === undefined) { + this.updatePermissionCache = this.accountService.getPermission(evalResource, SecurityAction.UPDATE); + } + return this.updatePermissionCache; + } + + getDeletePermission(evalResource: Resources): SecurityPermission { + if (this.deletePermissionCache === undefined) { + this.deletePermissionCache = this.accountService.getPermission(evalResource, SecurityAction.DELETE); + } + return this.deletePermissionCache; + } + + hasCreatePermission(evalResource: Resources): boolean { + return this.allowPermissions.includes(this.getCreatePermission(evalResource)); + } + + hasReadPermission(evalResource: Resources): boolean { + return this.allowPermissions.includes(this.getReadPermission(evalResource)); + } + + hasUpdatePermission(evalResource: Resources): boolean { + return this.allowPermissions.includes(this.getUpdatePermission(evalResource)); + } + + hasDeletePermission(evalResource: Resources): boolean { + return this.allowPermissions.includes(this.getDeletePermission(evalResource)); + } + + /* + * Action Permission methods. + * Evaluates if the current authenticated user AND/OR + * the current Entity instance allows the action to be done. Example, the User hasPermission to DELETE, + * but the Entity instance is in a state that DOESN'T allow it. + * In html template, if action permission fails, the element is showned but disabled. + * Usage: + * + * + * #1 : [routerLink]="!canUpdate(inputDataUpload()) ? null : ['/input-data-upload', inputDataUpload()!.id, 'edit']" + * Adds the link ONLY if the entity allows to be updated. Otherwise, only shows the value. + * #2 : [class.disabled]="!canUpdate(inputDataUpload())" + * Adds a css class if the entity dpesn't allows to be updated. This improves UX. + */ + canCreate(): boolean { + return true; + } + + canRead(entity: IE | null): boolean { + if (!entity) return false; + return this.service.canRead(entity); + } + + canUpdate(entity: IE | null): boolean { + if (!entity) return false; + return this.service.canUpdate(entity); + } + + canDelete(entity: IE | null): boolean { + if (!entity) return false; + return this.service.canDelete(entity); + } +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/resilient/resilient-validators-form.service.ts b/src/main/webapp/app/entities/resilient/resilient-validators-form.service.ts new file mode 100644 index 0000000..b10f686 --- /dev/null +++ b/src/main/webapp/app/entities/resilient/resilient-validators-form.service.ts @@ -0,0 +1,96 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +// Validator's to be applied to a FormGroup +export function dateRangeValidator(startDateField: string, endDateField: string): ValidatorFn { + return (formGroup: AbstractControl): ValidationErrors | null => { + const startDate = formGroup.get(startDateField)?.value; + const endDate = formGroup.get(endDateField)?.value; + + if (startDate && endDate && new Date(startDate) > new Date(endDate)) { + return { dateRangeInvalid: true }; + } + return null; + }; +} + +// Validator's to be applied to a field +// ==================================== + +// Validates that {thisDateControl} is BEFORE the {otherDateControl} +export function dateLTValidator(otherDateControlName: string): ValidatorFn { + return (thisDateControl: AbstractControl): ValidationErrors | null => { + const formGroup = thisDateControl.parent; + const otherDateControl = formGroup?.get(otherDateControlName); + + if (!thisDateControl || !otherDateControl) return null; + + const thisDateValue = thisDateControl?.value; + const otherDateValue = otherDateControl?.value; + + if (!thisDateValue || !otherDateValue) return null; + + if (thisDateValue && otherDateValue && new Date(thisDateValue) > new Date(otherDateValue)) { + // apply validation to the other field + otherDateControl.setErrors({ + dateGreaterThan: { + 'otherField': otherDateControlName, + 'dtThis': thisDateValue, + 'dtOther': otherDateValue + } + }); + + // apply validation to the validating field + return { + dateLessThan: { + 'otherField': otherDateControlName, + 'dtThis': thisDateValue, + 'dtOther': otherDateValue + } + }; + } else { + otherDateControl.setErrors(null); + // otherDateControl.updateValueAndValidity(); + return null; + } + }; +} + +// Validates that {thisDateControl} is AFTER the {otherDateControl} +export function dateGTValidator(otherDateControlName: string): ValidatorFn { + return (thisDateControl: AbstractControl): ValidationErrors | null => { + const formGroup = thisDateControl.parent; + const otherDateControl = formGroup?.get(otherDateControlName); + + if (!thisDateControl || !otherDateControl) return null; + + const thisDateValue = thisDateControl?.value; + const otherDateValue = otherDateControl?.value; + + if (!thisDateValue || !otherDateValue) return null; + + if (thisDateValue && otherDateValue && new Date(thisDateValue) < new Date(otherDateValue)) { + // apply validation to the other field + otherDateControl.setErrors({ + dateLessThan: { + 'otherField': otherDateControlName, + 'dtThis': thisDateValue, + 'dtOther': otherDateValue + } + }); + + // apply validation to the validating field + return { + dateGreaterThan: { + 'otherField': otherDateControlName, + 'dtThis': thisDateValue, + 'dtOther': otherDateValue + } + }; + } else { + otherDateControl.setErrors(null); + // otherDateControl.updateValueAndValidity(); + return null; + } + + }; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/shared/shared-search-form.service.ts b/src/main/webapp/app/entities/shared/shared-search-form.service.ts new file mode 100644 index 0000000..721c1d6 --- /dev/null +++ b/src/main/webapp/app/entities/shared/shared-search-form.service.ts @@ -0,0 +1,29 @@ +export class SearchFormService { + applySearchConditionsToQueryObject(queryObject: any, searchModel: any) { + if (searchModel) { + Object.entries(searchModel).forEach(([key, value]) => { + var clause: string = "contains"; //Default + if (value !== null && value !== undefined) { + switch(typeof value) { + case 'string': { + // Don't search with an empty string criteria + if (value.trim() === "") return; + clause = "contains"; + break; + } + case 'boolean': { + clause = "equals"; + break; + } + case 'number': { + clause = "equals"; + break; + } + } + + queryObject[key + "." + clause] = value; + } + }); + } + } +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.html b/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.html new file mode 100644 index 0000000..ae24847 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (unitConverter) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.spec.ts new file mode 100644 index 0000000..36c4ed8 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { UnitConverterService } from '../service/unit-converter.service'; + +import { UnitConverterDeleteDialogComponent } from './unit-converter-delete-dialog.component'; + +describe('UnitConverter Management Delete Component', () => { + let comp: UnitConverterDeleteDialogComponent; + let fixture: ComponentFixture; + let service: UnitConverterService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitConverterDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(UnitConverterDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(UnitConverterDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UnitConverterService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.ts b/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.ts new file mode 100644 index 0000000..32fe4f7 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/delete/unit-converter-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IUnitConverter } from '../unit-converter.model'; +import { UnitConverterService } from '../service/unit-converter.service'; + +@Component({ + standalone: true, + templateUrl: './unit-converter-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class UnitConverterDeleteDialogComponent { + unitConverter?: IUnitConverter; + + protected unitConverterService = inject(UnitConverterService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.unitConverterService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.html b/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.html new file mode 100644 index 0000000..759dd81 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.html @@ -0,0 +1,77 @@ + +
+
+ @if (unitConverter()) { +
+

Unit Converter

+ +
+ + + + + +
+
From Unit
+
+ @if (unitConverter()!.fromUnit) { + + } +
+
To Unit
+
+ @if (unitConverter()!.toUnit) { + + } +
+
Name
+
+ {{ unitConverter()!.name }} +
+
Description
+
+ {{ unitConverter()!.description }} +
+
+ Convertion Formula +
+
+ {{ unitConverter()!.convertionFormula }} +
+ + + +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.spec.ts b/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.spec.ts new file mode 100644 index 0000000..2d5c0ba --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { UnitConverterDetailComponent } from './unit-converter-detail.component'; + +describe('UnitConverter Management Detail Component', () => { + let comp: UnitConverterDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UnitConverterDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: UnitConverterDetailComponent, + resolve: { unitConverter: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(UnitConverterDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UnitConverterDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load unitConverter on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', UnitConverterDetailComponent); + + // THEN + expect(instance.unitConverter()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.ts b/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.ts new file mode 100644 index 0000000..354a697 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/detail/unit-converter-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IUnitConverter } from '../unit-converter.model'; + +@Component({ + standalone: true, + selector: 'jhi-unit-converter-detail', + templateUrl: './unit-converter-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class UnitConverterDetailComponent { + unitConverter = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/unit-converter/list/unit-converter-search-form.service.spec.ts b/src/main/webapp/app/entities/unit-converter/list/unit-converter-search-form.service.spec.ts new file mode 100644 index 0000000..98cd416 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/list/unit-converter-search-form.service.spec.ts @@ -0,0 +1,126 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../input-data.test-samples'; + +import { InputDataSearchFormService } from './input-data-search-form.service'; + +describe('InputData Search Form Service', () => { + let service: InputDataSearchFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(InputDataSearchFormService); + }); + + describe('Service methods', () => { + describe('createInputDatasearchFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createInputDataSearchFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + + it('passing SearchInputData should create a new form with FormGroup', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + sourceValue: expect.any(Object), + variableValue: expect.any(Object), + imputedValue: expect.any(Object), + sourceType: expect.any(Object), + dataDate: expect.any(Object), + changeDate: expect.any(Object), + changeUsername: expect.any(Object), + dataSource: expect.any(Object), + dataUser: expect.any(Object), + dataComments: expect.any(Object), + creationDate: expect.any(Object), + creationUsername: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + period: expect.any(Object), + periodVersion: expect.any(Object), + sourceUnit: expect.any(Object), + unit: expect.any(Object), + owner: expect.any(Object), + sourceInputData: expect.any(Object), + sourceInputDataUpload: expect.any(Object), + }), + ); + }); + }); + + describe('getInputData', () => { + it('should return SearchInputData for default InputData initial value', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithNewData); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject(sampleWithNewData); + }); + + it('should return SearchInputData for empty InputData initial value', () => { + const formGroup = service.createInputDataSearchFormGroup(); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject({}); + }); + + it('should return SearchInputData', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithRequiredData); + + const inputData = service.getInputData(formGroup) as any; + + expect(inputData).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IInputData should not enable id FormControl', () => { + const formGroup = service.createInputDataSearchFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing SearchInputData should disable id FormControl', () => { + const formGroup = service.createInputDataSearchFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/list/unit-converter-search-form.service.ts b/src/main/webapp/app/entities/unit-converter/list/unit-converter-search-form.service.ts new file mode 100644 index 0000000..296bd71 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/list/unit-converter-search-form.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { SearchUnitConverter } from '../unit-converter.model'; +import { SearchFormService } from 'app/entities/shared/shared-search-form.service'; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts SearchInputData, for search. in the list + */ +type UnitConverterSearchFormGroupInput = SearchUnitConverter; + +type UnitConverterSearchFormDefaults = SearchUnitConverter; + +type UnitConverterSearchFormGroupContent = { + id: FormControl; + name: FormControl; + description: FormControl; + fromUnitId: FormControl; + toUnitId: FormControl; +}; + +export type UnitConverterSearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class UnitConverterSearchFormService extends SearchFormService { + createUnitConverterSearchFormGroup(unitConverter: UnitConverterSearchFormGroupInput = { id: null }): UnitConverterSearchFormGroup { + const unitConverterRawValue = { + ...this.getSearchFormDefaults(), + ...unitConverter, + }; + return new FormGroup({ + id: new FormControl(unitConverterRawValue.id), + name: new FormControl(unitConverterRawValue.name), + description: new FormControl(unitConverterRawValue.description), + fromUnitId: new FormControl(unitConverterRawValue.fromUnitId), + toUnitId: new FormControl(unitConverterRawValue.toUnitId), + }); + } + + getUnitConverter(form: UnitConverterSearchFormGroup): SearchUnitConverter { + return form.getRawValue() as SearchUnitConverter; + } + + resetForm(form: UnitConverterSearchFormGroup, unitConverter: UnitConverterSearchFormGroupInput): void { + const unitConverterRawValue = { ...this.getSearchFormDefaults(), ...unitConverter }; + form.reset( + { + ...unitConverterRawValue, + id: { value: unitConverterRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): UnitConverterSearchFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.html b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.html new file mode 100644 index 0000000..2e0bad4 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.html @@ -0,0 +1,151 @@ + +
+

+ Unit Converters + +
+ + + +
+

+ + + + + + @if (unitConverters && unitConverters.length >= 0) { +
+ + {{ ' ' // Wrap the all table in a form. This is the only way reactiveforms will work }} +
+ + + + + + + + + + + + {{ ' ' // Search row }} + + + + + + + + + @for (unitConverter of unitConverters; track trackId) { + + + + + + + + } + +
+
+ From Unit + +
+
+
+ To Unit + +
+
+
+ Name + + +
+
+ @if (unitConverter.fromUnit) { + + } + + @if (unitConverter.toUnit) { + + } + {{ unitConverter.name }} +
+ + + Visualizar + + + + + Editar + + + +
+
+ +
+
+ } + + @if (unitConverters?.length === 0) { +
+ Nenhum Unit Converters encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.html.old b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.html.old new file mode 100644 index 0000000..efa31d5 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.html.old @@ -0,0 +1,136 @@ +
+

+ Unit Converters + +
+ + + +
+

+ + + + + + @if (unitConverters?.length === 0) { +
+ Nenhum Unit Converters encontrado +
+ } + + @if (unitConverters && unitConverters.length > 0) { +
+ + + + + + + + + + + + + + + @for (unitConverter of unitConverters; track trackId) { + + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Convertion Formula + + +
+
+
+ Version + + +
+
+
+ From Unit + +
+
+
+ To Unit + +
+
+ {{ unitConverter.id }} + {{ unitConverter.name }}{{ unitConverter.description }}{{ unitConverter.convertionFormula }}{{ unitConverter.version }} + @if (unitConverter.fromUnit) { + + } + + @if (unitConverter.toUnit) { + + } + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.spec.ts b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.spec.ts new file mode 100644 index 0000000..ce96723 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../unit-converter.test-samples'; +import { UnitConverterService } from '../service/unit-converter.service'; + +import { UnitConverterComponent } from './unit-converter.component'; +import SpyInstance = jest.SpyInstance; + +describe('UnitConverter Management Component', () => { + let comp: UnitConverterComponent; + let fixture: ComponentFixture; + let service: UnitConverterService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitConverterComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(UnitConverterComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(UnitConverterComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UnitConverterService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.unitConverters?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to unitConverterService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getUnitConverterIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getUnitConverterIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.ts b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.ts new file mode 100644 index 0000000..b4cd89b --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/list/unit-converter.component.ts @@ -0,0 +1,154 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, map } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IUnitConverter } from '../unit-converter.model'; +import { EntityArrayResponseType, UnitConverterService } from '../service/unit-converter.service'; +import { UnitConverterDeleteDialogComponent } from '../delete/unit-converter-delete-dialog.component'; + +import { UnitConverterSearchFormService, UnitConverterSearchFormGroup } from './unit-converter-search-form.service'; +import { IUnit } from 'app/entities/unit/unit.model'; +import { UnitService } from 'app/entities/unit/service/unit.service'; + +@Component({ + standalone: true, + selector: 'jhi-unit-converter', + templateUrl: './unit-converter.component.html', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class UnitConverterComponent implements OnInit { + subscription: Subscription | null = null; + unitConverters?: IUnitConverter[]; + isLoading = false; + + sortState = sortStateSignal({}); + + unitsSharedCollection: IUnit[] = []; + + public router = inject(Router); + protected unitService = inject(UnitService); + protected unitConverterService = inject(UnitConverterService); + protected unitConverterSearchFormService = inject(UnitConverterSearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + searchForm: UnitConverterSearchFormGroup = this.unitConverterSearchFormService.createUnitConverterSearchFormGroup(); + + trackId = (_index: number, item: IUnitConverter): number => this.unitConverterService.getEntityIdentifier(item); + + compareUnit = (o1: IUnit | null, o2: IUnit | null): boolean => + this.unitService.compareEntity(o1, o2); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.unitConverters || this.unitConverters.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + + this.loadRelationshipsOptions(); + } + + delete(unitConverter: IUnitConverter): void { + const modalRef = this.modalService.open(UnitConverterDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.unitConverter = unitConverter; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + onSearch(): void { + this.load(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.unitConverters = this.refineData(dataFromBody); + } + + protected refineData(data: IUnitConverter[]): IUnitConverter[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IUnitConverter[] | null): IUnitConverter[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + + const searchModel = this.unitConverterSearchFormService.getUnitConverter(this.searchForm); + this.unitConverterSearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + + return this.unitConverterService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } + + protected loadRelationshipsOptions(): void { + this.unitService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((units: IUnit[]) => (this.unitsSharedCollection = units)); + } +} diff --git a/src/main/webapp/app/entities/unit-converter/route/unit-converter-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/unit-converter/route/unit-converter-routing-resolve.service.spec.ts new file mode 100644 index 0000000..f3549f7 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/route/unit-converter-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IUnitConverter } from '../unit-converter.model'; +import { UnitConverterService } from '../service/unit-converter.service'; + +import unitConverterResolve from './unit-converter-routing-resolve.service'; + +describe('UnitConverter routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: UnitConverterService; + let resultUnitConverter: IUnitConverter | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(UnitConverterService); + resultUnitConverter = undefined; + }); + + describe('resolve', () => { + it('should return IUnitConverter returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + unitConverterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnitConverter = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultUnitConverter).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + unitConverterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnitConverter = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultUnitConverter).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + unitConverterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnitConverter = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultUnitConverter).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/route/unit-converter-routing-resolve.service.ts b/src/main/webapp/app/entities/unit-converter/route/unit-converter-routing-resolve.service.ts new file mode 100644 index 0000000..d4acb41 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/route/unit-converter-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IUnitConverter } from '../unit-converter.model'; +import { UnitConverterService } from '../service/unit-converter.service'; + +const unitConverterResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(UnitConverterService) + .find(id) + .pipe( + mergeMap((unitConverter: HttpResponse) => { + if (unitConverter.body) { + return of(unitConverter.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default unitConverterResolve; diff --git a/src/main/webapp/app/entities/unit-converter/service/unit-converter.service.spec.ts b/src/main/webapp/app/entities/unit-converter/service/unit-converter.service.spec.ts new file mode 100644 index 0000000..d4fd96b --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/service/unit-converter.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IUnitConverter } from '../unit-converter.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../unit-converter.test-samples'; + +import { UnitConverterService } from './unit-converter.service'; + +const requireRestSample: IUnitConverter = { + ...sampleWithRequiredData, +}; + +describe('UnitConverter Service', () => { + let service: UnitConverterService; + let httpMock: HttpTestingController; + let expectedResult: IUnitConverter | IUnitConverter[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(UnitConverterService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a UnitConverter', () => { + const unitConverter = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(unitConverter).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a UnitConverter', () => { + const unitConverter = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(unitConverter).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a UnitConverter', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of UnitConverter', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a UnitConverter', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addUnitConverterToCollectionIfMissing', () => { + it('should add a UnitConverter to an empty array', () => { + const unitConverter: IUnitConverter = sampleWithRequiredData; + expectedResult = service.addUnitConverterToCollectionIfMissing([], unitConverter); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(unitConverter); + }); + + it('should not add a UnitConverter to an array that contains it', () => { + const unitConverter: IUnitConverter = sampleWithRequiredData; + const unitConverterCollection: IUnitConverter[] = [ + { + ...unitConverter, + }, + sampleWithPartialData, + ]; + expectedResult = service.addUnitConverterToCollectionIfMissing(unitConverterCollection, unitConverter); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a UnitConverter to an array that doesn't contain it", () => { + const unitConverter: IUnitConverter = sampleWithRequiredData; + const unitConverterCollection: IUnitConverter[] = [sampleWithPartialData]; + expectedResult = service.addUnitConverterToCollectionIfMissing(unitConverterCollection, unitConverter); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(unitConverter); + }); + + it('should add only unique UnitConverter to an array', () => { + const unitConverterArray: IUnitConverter[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const unitConverterCollection: IUnitConverter[] = [sampleWithRequiredData]; + expectedResult = service.addUnitConverterToCollectionIfMissing(unitConverterCollection, ...unitConverterArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const unitConverter: IUnitConverter = sampleWithRequiredData; + const unitConverter2: IUnitConverter = sampleWithPartialData; + expectedResult = service.addUnitConverterToCollectionIfMissing([], unitConverter, unitConverter2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(unitConverter); + expect(expectedResult).toContain(unitConverter2); + }); + + it('should accept null and undefined values', () => { + const unitConverter: IUnitConverter = sampleWithRequiredData; + expectedResult = service.addUnitConverterToCollectionIfMissing([], null, unitConverter, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(unitConverter); + }); + + it('should return initial array if no UnitConverter is added', () => { + const unitConverterCollection: IUnitConverter[] = [sampleWithRequiredData]; + expectedResult = service.addUnitConverterToCollectionIfMissing(unitConverterCollection, undefined, null); + expect(expectedResult).toEqual(unitConverterCollection); + }); + }); + + describe('compareUnitConverter', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareUnitConverter(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareUnitConverter(entity1, entity2); + const compareResult2 = service.compareUnitConverter(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareUnitConverter(entity1, entity2); + const compareResult2 = service.compareUnitConverter(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareUnitConverter(entity1, entity2); + const compareResult2 = service.compareUnitConverter(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/service/unit-converter.service.ts b/src/main/webapp/app/entities/unit-converter/service/unit-converter.service.ts new file mode 100644 index 0000000..a9d7802 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/service/unit-converter.service.ts @@ -0,0 +1,33 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IUnitConverter, NewUnitConverter } from '../unit-converter.model'; + +export type PartialUpdateUnitConverter = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class UnitConverterService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/unit-converters'); + getResourceUrl(): string { return this.resourceUrl; } + + // Get a list of UnitConverters, that applies to fromUnit and toUnit + queryByFromAndTo(fromUnitId: number, toUnitId: number): Observable { + return this.http.get(`${this.resourceUrl}/from/${fromUnitId}/to/${toUnitId}`, { observe: 'response' }); + } + + // Converts a value from a Unit, to the variable base unit + convertValue(periodId: number, variableId: number, variableClassCode: string | null, fromUnitId: number, value: number): Observable> { + return this.http.get(`${this.resourceUrl}/convert/${periodId}/${variableId}/${variableClassCode}/${fromUnitId}/${value}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/unit-converter/unit-converter.model.ts b/src/main/webapp/app/entities/unit-converter/unit-converter.model.ts new file mode 100644 index 0000000..9ed6125 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/unit-converter.model.ts @@ -0,0 +1,27 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IUnit } from 'app/entities/unit/unit.model'; + +export interface IUnitConverter extends IResilientEntity { + name?: string | null; + description?: string | null; + convertionFormula?: string | null; + fromUnit?: Pick | null; + toUnit?: Pick | null; +} + +export type NewUnitConverter = Omit & { id: null }; + +export type SearchUnitConverter = Omit< + IUnitConverter, + // Remove unwanted properties for search + 'id' | 'convertionFormula' | 'fromUnit' | 'toUnit' | 'version'> + & + // Add specific properties for search + { + id?: number | null; + name?: string | null; + description?: string | null; + fromUnitId?: number | null; + toUnitId?: number | null; + }; diff --git a/src/main/webapp/app/entities/unit-converter/unit-converter.routes.ts b/src/main/webapp/app/entities/unit-converter/unit-converter.routes.ts new file mode 100644 index 0000000..c5ef449 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/unit-converter.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { UnitConverterComponent } from './list/unit-converter.component'; +import { UnitConverterDetailComponent } from './detail/unit-converter-detail.component'; +import { UnitConverterUpdateComponent } from './update/unit-converter-update.component'; +import UnitConverterResolve from './route/unit-converter-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { UnitConverterService } from './service/unit-converter.service'; +import { Resources } from 'app/security/resources.model'; + +const unitConverterRoute: Routes = [ + { + path: '', + component: UnitConverterComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: UnitConverterService, + resource: Resources.UNIT_CONVERTER, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: UnitConverterDetailComponent, + resolve: { + unitConverter: UnitConverterResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: UnitConverterService, + resource: Resources.UNIT_CONVERTER, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: UnitConverterUpdateComponent, + resolve: { + unitConverter: UnitConverterResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: UnitConverterService, + resource: Resources.UNIT_CONVERTER, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: UnitConverterUpdateComponent, + resolve: { + unitConverter: UnitConverterResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: UnitConverterService, + resource: Resources.UNIT_CONVERTER, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default unitConverterRoute; diff --git a/src/main/webapp/app/entities/unit-converter/unit-converter.test-samples.ts b/src/main/webapp/app/entities/unit-converter/unit-converter.test-samples.ts new file mode 100644 index 0000000..015d6ce --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/unit-converter.test-samples.ts @@ -0,0 +1,35 @@ +import { IUnitConverter, NewUnitConverter } from './unit-converter.model'; + +export const sampleWithRequiredData: IUnitConverter = { + id: 23016, + name: 'cheerfully wetly', + description: 'above next furiously', + convertionFormula: 'er blah meanwhile', +}; + +export const sampleWithPartialData: IUnitConverter = { + id: 2152, + name: 'turbocharge or civilization', + description: 'astride', + convertionFormula: 'havoc', +}; + +export const sampleWithFullData: IUnitConverter = { + id: 28481, + name: 'foul', + description: 'plus mostly distinct', + convertionFormula: 'mislead respect', + version: 43, +}; + +export const sampleWithNewData: NewUnitConverter = { + name: 'interesting', + description: 'naturally', + convertionFormula: 'charity', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/unit-converter/update/unit-converter-form.service.spec.ts b/src/main/webapp/app/entities/unit-converter/update/unit-converter-form.service.spec.ts new file mode 100644 index 0000000..ad74fda --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/update/unit-converter-form.service.spec.ts @@ -0,0 +1,96 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../unit-converter.test-samples'; + +import { UnitConverterFormService } from './unit-converter-form.service'; + +describe('UnitConverter Form Service', () => { + let service: UnitConverterFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UnitConverterFormService); + }); + + describe('Service methods', () => { + describe('createUnitConverterFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createUnitConverterFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + convertionFormula: expect.any(Object), + version: expect.any(Object), + fromUnit: expect.any(Object), + toUnit: expect.any(Object), + }), + ); + }); + + it('passing IUnitConverter should create a new form with FormGroup', () => { + const formGroup = service.createUnitConverterFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + convertionFormula: expect.any(Object), + version: expect.any(Object), + fromUnit: expect.any(Object), + toUnit: expect.any(Object), + }), + ); + }); + }); + + describe('getUnitConverter', () => { + it('should return NewUnitConverter for default UnitConverter initial value', () => { + const formGroup = service.createUnitConverterFormGroup(sampleWithNewData); + + const unitConverter = service.getUnitConverter(formGroup) as any; + + expect(unitConverter).toMatchObject(sampleWithNewData); + }); + + it('should return NewUnitConverter for empty UnitConverter initial value', () => { + const formGroup = service.createUnitConverterFormGroup(); + + const unitConverter = service.getUnitConverter(formGroup) as any; + + expect(unitConverter).toMatchObject({}); + }); + + it('should return IUnitConverter', () => { + const formGroup = service.createUnitConverterFormGroup(sampleWithRequiredData); + + const unitConverter = service.getUnitConverter(formGroup) as any; + + expect(unitConverter).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IUnitConverter should not enable id FormControl', () => { + const formGroup = service.createUnitConverterFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewUnitConverter should disable id FormControl', () => { + const formGroup = service.createUnitConverterFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/update/unit-converter-form.service.ts b/src/main/webapp/app/entities/unit-converter/update/unit-converter-form.service.ts new file mode 100644 index 0000000..4e7f391 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/update/unit-converter-form.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IUnitConverter, NewUnitConverter } from '../unit-converter.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IUnitConverter for edit and NewUnitConverterFormGroupInput for create. + */ +type UnitConverterFormGroupInput = IUnitConverter | PartialWithRequiredKeyOf; + +type UnitConverterFormDefaults = Pick; + +type UnitConverterFormGroupContent = { + id: FormControl; + name: FormControl; + description: FormControl; + convertionFormula: FormControl; + version: FormControl; + fromUnit: FormControl; + toUnit: FormControl; +}; + +export type UnitConverterFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class UnitConverterFormService { + createUnitConverterFormGroup(unitConverter: UnitConverterFormGroupInput = { id: null }): UnitConverterFormGroup { + const unitConverterRawValue = { + ...this.getFormDefaults(), + ...unitConverter, + }; + return new FormGroup({ + id: new FormControl( + { value: unitConverterRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + name: new FormControl(unitConverterRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(unitConverterRawValue.description), + convertionFormula: new FormControl(unitConverterRawValue.convertionFormula, { + validators: [Validators.required], + }), + version: new FormControl(unitConverterRawValue.version), + fromUnit: new FormControl(unitConverterRawValue.fromUnit), + toUnit: new FormControl(unitConverterRawValue.toUnit), + }); + } + + getUnitConverter(form: UnitConverterFormGroup): IUnitConverter | NewUnitConverter { + return form.getRawValue() as IUnitConverter | NewUnitConverter; + } + + resetForm(form: UnitConverterFormGroup, unitConverter: UnitConverterFormGroupInput): void { + const unitConverterRawValue = { ...this.getFormDefaults(), ...unitConverter }; + form.reset( + { + ...unitConverterRawValue, + id: { value: unitConverterRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): UnitConverterFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.css b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.css new file mode 100644 index 0000000..a620fa3 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.css @@ -0,0 +1,8 @@ +span.help-formula-toggle { + float: right; + cursor: pointer; +} + +#collapseSection .card-body { + margin-bottom: 5px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.html b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.html new file mode 100644 index 0000000..82f3011 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.html @@ -0,0 +1,162 @@ + +
+
+
+

+ Criar ou editar Unit Converter +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + +
+
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+
+ + + {{ '' // Add fa-icon 'faCaretDown' to /app/config/font-awesome-icons.ts }} + {{ '' // Add fa-icon 'faCaretUp' to /app/config/font-awesome-icons.ts }} + help +
+ +
+
+
Formula helper
+

Formula fields provide a powerfull way of configuration and calculation. Use any of SpEL language anf funtions.

+

In this context, the following functions are added:

+
    +
  • #value - the value to be converted
  • +
  • #fromUnit - the Unit in wich the value is expressed
  • +
  • #toUnit - the target Unit, that the value will be converted
  • +
  • #variable - the current Variable instance
  • +
  • #classCode - the class code selected, if any. (might be empty)
  • +
+
+
+ + + +
+ + + +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.spec.ts b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.spec.ts new file mode 100644 index 0000000..05750a0 --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IUnit } from 'app/entities/unit/unit.model'; +import { UnitService } from 'app/entities/unit/service/unit.service'; +import { UnitConverterService } from '../service/unit-converter.service'; +import { IUnitConverter } from '../unit-converter.model'; +import { UnitConverterFormService } from './unit-converter-form.service'; + +import { UnitConverterUpdateComponent } from './unit-converter-update.component'; + +describe('UnitConverter Management Update Component', () => { + let comp: UnitConverterUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let unitConverterFormService: UnitConverterFormService; + let unitConverterService: UnitConverterService; + let unitService: UnitService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitConverterUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(UnitConverterUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(UnitConverterUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + unitConverterFormService = TestBed.inject(UnitConverterFormService); + unitConverterService = TestBed.inject(UnitConverterService); + unitService = TestBed.inject(UnitService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call Unit query and add missing value', () => { + const unitConverter: IUnitConverter = { id: 456 }; + const fromUnit: IUnit = { id: 8943 }; + unitConverter.fromUnit = fromUnit; + const toUnit: IUnit = { id: 23724 }; + unitConverter.toUnit = toUnit; + + const unitCollection: IUnit[] = [{ id: 7591 }]; + jest.spyOn(unitService, 'query').mockReturnValue(of(new HttpResponse({ body: unitCollection }))); + const additionalUnits = [fromUnit, toUnit]; + const expectedCollection: IUnit[] = [...additionalUnits, ...unitCollection]; + jest.spyOn(unitService, 'addUnitToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ unitConverter }); + comp.ngOnInit(); + + expect(unitService.query).toHaveBeenCalled(); + expect(unitService.addUnitToCollectionIfMissing).toHaveBeenCalledWith( + unitCollection, + ...additionalUnits.map(expect.objectContaining), + ); + expect(comp.unitsSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const unitConverter: IUnitConverter = { id: 456 }; + const fromUnit: IUnit = { id: 12548 }; + unitConverter.fromUnit = fromUnit; + const toUnit: IUnit = { id: 19205 }; + unitConverter.toUnit = toUnit; + + activatedRoute.data = of({ unitConverter }); + comp.ngOnInit(); + + expect(comp.unitsSharedCollection).toContain(fromUnit); + expect(comp.unitsSharedCollection).toContain(toUnit); + expect(comp.unitConverter).toEqual(unitConverter); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const unitConverter = { id: 123 }; + jest.spyOn(unitConverterFormService, 'getUnitConverter').mockReturnValue(unitConverter); + jest.spyOn(unitConverterService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unitConverter }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: unitConverter })); + saveSubject.complete(); + + // THEN + expect(unitConverterFormService.getUnitConverter).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(unitConverterService.update).toHaveBeenCalledWith(expect.objectContaining(unitConverter)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const unitConverter = { id: 123 }; + jest.spyOn(unitConverterFormService, 'getUnitConverter').mockReturnValue({ id: null }); + jest.spyOn(unitConverterService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unitConverter: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: unitConverter })); + saveSubject.complete(); + + // THEN + expect(unitConverterFormService.getUnitConverter).toHaveBeenCalled(); + expect(unitConverterService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const unitConverter = { id: 123 }; + jest.spyOn(unitConverterService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unitConverter }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(unitConverterService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareUnit', () => { + it('Should forward to unitService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(unitService, 'compareUnit'); + comp.compareUnit(entity, entity2); + expect(unitService.compareUnit).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.ts b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.ts new file mode 100644 index 0000000..6ea036a --- /dev/null +++ b/src/main/webapp/app/entities/unit-converter/update/unit-converter-update.component.ts @@ -0,0 +1,112 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms'; + +import { IUnit } from 'app/entities/unit/unit.model'; +import { UnitService } from 'app/entities/unit/service/unit.service'; +import { IUnitConverter } from '../unit-converter.model'; +import { UnitConverterService } from '../service/unit-converter.service'; +import { UnitConverterFormService, UnitConverterFormGroup } from './unit-converter-form.service'; + +import { CodeMirrorFieldComponent } from 'app/resilient/codemirrorfield/codemirrorfield.component' + +@Component({ + standalone: true, + selector: 'jhi-unit-converter-update', + templateUrl: './unit-converter-update.component.html', + styleUrl: './unit-converter-update.component.css', + imports: [SharedModule, FormsModule, ReactiveFormsModule, CodeMirrorFieldComponent], +}) +export class UnitConverterUpdateComponent implements OnInit { + isSaving = false; + isCollapsed = true; + unitConverter: IUnitConverter | null = null; + + unitsSharedCollection: IUnit[] = []; + + protected unitConverterService = inject(UnitConverterService); + protected unitConverterFormService = inject(UnitConverterFormService); + protected unitService = inject(UnitService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: UnitConverterFormGroup = this.unitConverterFormService.createUnitConverterFormGroup(); + + compareUnit = (o1: IUnit | null, o2: IUnit | null): boolean => this.unitService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ unitConverter }) => { + this.unitConverter = unitConverter; + if (unitConverter) { + this.updateForm(unitConverter); + } + + this.loadRelationshipsOptions(); + }); + } + + toggleCollapse(): void { + this.isCollapsed = !this.isCollapsed; + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const unitConverter = this.unitConverterFormService.getUnitConverter(this.editForm); + if (unitConverter.id !== null) { + this.subscribeToSaveResponse(this.unitConverterService.update(unitConverter)); + } else { + this.subscribeToSaveResponse(this.unitConverterService.create(unitConverter)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(unitConverter: IUnitConverter): void { + this.unitConverter = unitConverter; + this.unitConverterFormService.resetForm(this.editForm, unitConverter); + + this.unitsSharedCollection = this.unitService.addEntityToCollectionIfMissing( + this.unitsSharedCollection, + unitConverter.fromUnit, + unitConverter.toUnit, + ); + } + + protected loadRelationshipsOptions(): void { + this.unitService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((units: IUnit[]) => + this.unitService.addEntityToCollectionIfMissing(units, this.unitConverter?.fromUnit, this.unitConverter?.toUnit), + ), + ) + .subscribe((units: IUnit[]) => (this.unitsSharedCollection = units)); + } +} diff --git a/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.html b/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.html new file mode 100644 index 0000000..02f15de --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (unitType) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.spec.ts new file mode 100644 index 0000000..0ab1850 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { UnitTypeService } from '../service/unit-type.service'; + +import { UnitTypeDeleteDialogComponent } from './unit-type-delete-dialog.component'; + +describe('UnitType Management Delete Component', () => { + let comp: UnitTypeDeleteDialogComponent; + let fixture: ComponentFixture; + let service: UnitTypeService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitTypeDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(UnitTypeDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(UnitTypeDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UnitTypeService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.ts b/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.ts new file mode 100644 index 0000000..c6b374e --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/delete/unit-type-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IUnitType } from '../unit-type.model'; +import { UnitTypeService } from '../service/unit-type.service'; + +@Component({ + standalone: true, + templateUrl: './unit-type-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class UnitTypeDeleteDialogComponent { + unitType?: IUnitType; + + protected unitTypeService = inject(UnitTypeService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.unitTypeService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.html b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.html new file mode 100644 index 0000000..446e87a --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.html @@ -0,0 +1,57 @@ + +
+
+ @if (unitType()) { +
+

Unit Type

+ +
+ + + + + +
+
Name
+
+ {{ unitType()!.name }} +
+
Description
+
+ {{ unitType()!.description }} +
+
Value Type
+
+ {{ + { null: '', DECIMAL: 'DECIMAL', BOOLEAN: 'BOOLEAN', STRING: 'STRING' }[unitType()!.valueType ?? 'null'] + }} +
+ + + +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.html.old b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.html.old new file mode 100644 index 0000000..4476c0a --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.html.old @@ -0,0 +1,52 @@ +
+
+ @if (unitType()) { +
+

Unit Type

+ +
+ + + + + +
+
Código
+
+ {{ unitType()!.id }} +
+
Name
+
+ {{ unitType()!.name }} +
+
Description
+
+ {{ unitType()!.description }} +
+
Value Type
+
+ {{ + { null: '', DECIMAL: 'DECIMAL', BOOLEAN: 'BOOLEAN' }[unitType()!.valueType ?? 'null'] + }} +
+
+ Version +
+
+ {{ unitType()!.version }} +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.spec.ts b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.spec.ts new file mode 100644 index 0000000..36ac181 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { UnitTypeDetailComponent } from './unit-type-detail.component'; + +describe('UnitType Management Detail Component', () => { + let comp: UnitTypeDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UnitTypeDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: UnitTypeDetailComponent, + resolve: { unitType: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(UnitTypeDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UnitTypeDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load unitType on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', UnitTypeDetailComponent); + + // THEN + expect(instance.unitType()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.ts b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.ts new file mode 100644 index 0000000..2461d66 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/detail/unit-type-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IUnitType } from '../unit-type.model'; + +@Component({ + standalone: true, + selector: 'jhi-unit-type-detail', + templateUrl: './unit-type-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class UnitTypeDetailComponent { + unitType = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/unit-type/list/unit-type.component.html b/src/main/webapp/app/entities/unit-type/list/unit-type.component.html new file mode 100644 index 0000000..64b0fb5 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/list/unit-type.component.html @@ -0,0 +1,98 @@ + +
+

+ Unit Types + +
+ + + +
+

+ + + + + + @if (unitTypes?.length === 0) { +
+ Nenhum Unit Types encontrado +
+ } + + @if (unitTypes && unitTypes.length > 0) { +
+ + + + + + + + + + + + @for (unitType of unitTypes; track trackId) { + + + + + + + + } + +
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Value Type + + +
+
{{ unitType.name }}{{ unitType.description }} + {{ { null: '', DECIMAL: 'DECIMAL', BOOLEAN: 'BOOLEAN', STRING: 'STRING' }[unitType.valueType ?? 'null'] }} + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/unit-type/list/unit-type.component.spec.ts b/src/main/webapp/app/entities/unit-type/list/unit-type.component.spec.ts new file mode 100644 index 0000000..e04c4e1 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/list/unit-type.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../unit-type.test-samples'; +import { UnitTypeService } from '../service/unit-type.service'; + +import { UnitTypeComponent } from './unit-type.component'; +import SpyInstance = jest.SpyInstance; + +describe('UnitType Management Component', () => { + let comp: UnitTypeComponent; + let fixture: ComponentFixture; + let service: UnitTypeService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitTypeComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(UnitTypeComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(UnitTypeComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UnitTypeService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.unitTypes?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to unitTypeService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getUnitTypeIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getUnitTypeIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/unit-type/list/unit-type.component.ts b/src/main/webapp/app/entities/unit-type/list/unit-type.component.ts new file mode 100644 index 0000000..885b187 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/list/unit-type.component.ts @@ -0,0 +1,123 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IUnitType } from '../unit-type.model'; +import { EntityArrayResponseType, UnitTypeService } from '../service/unit-type.service'; +import { UnitTypeDeleteDialogComponent } from '../delete/unit-type-delete-dialog.component'; + +import { ResilientBaseComponent } from 'app/resilient/resilient-base/resilient-base.component'; + +@Component({ + standalone: true, + selector: 'jhi-unit-type', + templateUrl: './unit-type.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class UnitTypeComponent extends ResilientBaseComponent implements OnInit { + subscription: Subscription | null = null; + unitTypes?: IUnitType[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected unitTypeService = inject(UnitTypeService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IUnitType): number => this.unitTypeService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.unitTypes || this.unitTypes.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(unitType: IUnitType): void { + const modalRef = this.modalService.open(UnitTypeDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.unitType = unitType; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.unitTypes = this.refineData(dataFromBody); + } + + protected refineData(data: IUnitType[]): IUnitType[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IUnitType[] | null): IUnitType[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.unitTypeService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/unit-type/route/unit-type-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/unit-type/route/unit-type-routing-resolve.service.spec.ts new file mode 100644 index 0000000..bcf5bfc --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/route/unit-type-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IUnitType } from '../unit-type.model'; +import { UnitTypeService } from '../service/unit-type.service'; + +import unitTypeResolve from './unit-type-routing-resolve.service'; + +describe('UnitType routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: UnitTypeService; + let resultUnitType: IUnitType | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(UnitTypeService); + resultUnitType = undefined; + }); + + describe('resolve', () => { + it('should return IUnitType returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + unitTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnitType = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultUnitType).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + unitTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnitType = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultUnitType).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + unitTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnitType = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultUnitType).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-type/route/unit-type-routing-resolve.service.ts b/src/main/webapp/app/entities/unit-type/route/unit-type-routing-resolve.service.ts new file mode 100644 index 0000000..f5d72fe --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/route/unit-type-routing-resolve.service.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IUnitType } from '../unit-type.model'; +import { UnitTypeService } from '../service/unit-type.service'; + +const unitTypeResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(UnitTypeService) + .find(id) + .pipe( + mergeMap((unitType: HttpResponse) => { + if (unitType.body) { + const ut = of(unitType.body); + return of(unitType.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default unitTypeResolve; diff --git a/src/main/webapp/app/entities/unit-type/service/unit-type.service.spec.ts b/src/main/webapp/app/entities/unit-type/service/unit-type.service.spec.ts new file mode 100644 index 0000000..39ad792 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/service/unit-type.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IUnitType } from '../unit-type.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../unit-type.test-samples'; + +import { UnitTypeService } from './unit-type.service'; + +const requireRestSample: IUnitType = { + ...sampleWithRequiredData, +}; + +describe('UnitType Service', () => { + let service: UnitTypeService; + let httpMock: HttpTestingController; + let expectedResult: IUnitType | IUnitType[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(UnitTypeService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a UnitType', () => { + const unitType = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(unitType).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a UnitType', () => { + const unitType = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(unitType).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a UnitType', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of UnitType', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a UnitType', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addUnitTypeToCollectionIfMissing', () => { + it('should add a UnitType to an empty array', () => { + const unitType: IUnitType = sampleWithRequiredData; + expectedResult = service.addUnitTypeToCollectionIfMissing([], unitType); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(unitType); + }); + + it('should not add a UnitType to an array that contains it', () => { + const unitType: IUnitType = sampleWithRequiredData; + const unitTypeCollection: IUnitType[] = [ + { + ...unitType, + }, + sampleWithPartialData, + ]; + expectedResult = service.addUnitTypeToCollectionIfMissing(unitTypeCollection, unitType); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a UnitType to an array that doesn't contain it", () => { + const unitType: IUnitType = sampleWithRequiredData; + const unitTypeCollection: IUnitType[] = [sampleWithPartialData]; + expectedResult = service.addUnitTypeToCollectionIfMissing(unitTypeCollection, unitType); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(unitType); + }); + + it('should add only unique UnitType to an array', () => { + const unitTypeArray: IUnitType[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const unitTypeCollection: IUnitType[] = [sampleWithRequiredData]; + expectedResult = service.addUnitTypeToCollectionIfMissing(unitTypeCollection, ...unitTypeArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const unitType: IUnitType = sampleWithRequiredData; + const unitType2: IUnitType = sampleWithPartialData; + expectedResult = service.addUnitTypeToCollectionIfMissing([], unitType, unitType2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(unitType); + expect(expectedResult).toContain(unitType2); + }); + + it('should accept null and undefined values', () => { + const unitType: IUnitType = sampleWithRequiredData; + expectedResult = service.addUnitTypeToCollectionIfMissing([], null, unitType, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(unitType); + }); + + it('should return initial array if no UnitType is added', () => { + const unitTypeCollection: IUnitType[] = [sampleWithRequiredData]; + expectedResult = service.addUnitTypeToCollectionIfMissing(unitTypeCollection, undefined, null); + expect(expectedResult).toEqual(unitTypeCollection); + }); + }); + + describe('compareUnitType', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareUnitType(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareUnitType(entity1, entity2); + const compareResult2 = service.compareUnitType(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareUnitType(entity1, entity2); + const compareResult2 = service.compareUnitType(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareUnitType(entity1, entity2); + const compareResult2 = service.compareUnitType(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/unit-type/service/unit-type.service.ts b/src/main/webapp/app/entities/unit-type/service/unit-type.service.ts new file mode 100644 index 0000000..c68f8f9 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/service/unit-type.service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IUnitType, NewUnitType } from '../unit-type.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class UnitTypeService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/unit-types'); + getResourceUrl(): string { return this.resourceUrl; } +} diff --git a/src/main/webapp/app/entities/unit-type/unit-type.model.ts b/src/main/webapp/app/entities/unit-type/unit-type.model.ts new file mode 100644 index 0000000..4b4c884 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/unit-type.model.ts @@ -0,0 +1,11 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { UnitValueType } from 'app/entities/enumerations/unit-value-type.model'; + +export interface IUnitType extends IResilientEntity { + name?: string | null; + description?: string | null; + valueType?: keyof typeof UnitValueType | null; +} + +export type NewUnitType = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/unit-type/unit-type.routes.ts b/src/main/webapp/app/entities/unit-type/unit-type.routes.ts new file mode 100644 index 0000000..b6dd10b --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/unit-type.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { UnitTypeComponent } from './list/unit-type.component'; +import { UnitTypeDetailComponent } from './detail/unit-type-detail.component'; +import { UnitTypeUpdateComponent } from './update/unit-type-update.component'; +import UnitTypeResolve from './route/unit-type-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { UnitTypeService } from './service/unit-type.service'; +import { Resources } from 'app/security/resources.model'; + +const unitTypeRoute: Routes = [ + { + path: '', + component: UnitTypeComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: UnitTypeService, + resource: Resources.UNIT_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: UnitTypeDetailComponent, + resolve: { + unitType: UnitTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: UnitTypeService, + resource: Resources.UNIT_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: UnitTypeUpdateComponent, + resolve: { + unitType: UnitTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: UnitTypeService, + resource: Resources.UNIT_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: UnitTypeUpdateComponent, + resolve: { + unitType: UnitTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: UnitTypeService, + resource: Resources.UNIT_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default unitTypeRoute; diff --git a/src/main/webapp/app/entities/unit-type/unit-type.test-samples.ts b/src/main/webapp/app/entities/unit-type/unit-type.test-samples.ts new file mode 100644 index 0000000..dd29332 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/unit-type.test-samples.ts @@ -0,0 +1,35 @@ +import { IUnitType, NewUnitType } from './unit-type.model'; + +export const sampleWithRequiredData: IUnitType = { + id: 19315, + name: 'except barring upset', + description: 'hearty ew um', + valueType: 'DECIMAL', +}; + +export const sampleWithPartialData: IUnitType = { + id: 22736, + name: 'except hm', + description: 'upon push deserted', + valueType: 'BOOLEAN', +}; + +export const sampleWithFullData: IUnitType = { + id: 579, + name: 'tatami', + description: 'barring really', + valueType: 'DECIMAL', + version: 28677, +}; + +export const sampleWithNewData: NewUnitType = { + name: 'reckless outgun', + description: 'though', + valueType: 'BOOLEAN', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/unit-type/update/unit-type-form.service.spec.ts b/src/main/webapp/app/entities/unit-type/update/unit-type-form.service.spec.ts new file mode 100644 index 0000000..e46a22e --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/update/unit-type-form.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../unit-type.test-samples'; + +import { UnitTypeFormService } from './unit-type-form.service'; + +describe('UnitType Form Service', () => { + let service: UnitTypeFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UnitTypeFormService); + }); + + describe('Service methods', () => { + describe('createUnitTypeFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createUnitTypeFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + valueType: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + + it('passing IUnitType should create a new form with FormGroup', () => { + const formGroup = service.createUnitTypeFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + valueType: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + }); + + describe('getUnitType', () => { + it('should return NewUnitType for default UnitType initial value', () => { + const formGroup = service.createUnitTypeFormGroup(sampleWithNewData); + + const unitType = service.getUnitType(formGroup) as any; + + expect(unitType).toMatchObject(sampleWithNewData); + }); + + it('should return NewUnitType for empty UnitType initial value', () => { + const formGroup = service.createUnitTypeFormGroup(); + + const unitType = service.getUnitType(formGroup) as any; + + expect(unitType).toMatchObject({}); + }); + + it('should return IUnitType', () => { + const formGroup = service.createUnitTypeFormGroup(sampleWithRequiredData); + + const unitType = service.getUnitType(formGroup) as any; + + expect(unitType).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IUnitType should not enable id FormControl', () => { + const formGroup = service.createUnitTypeFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewUnitType should disable id FormControl', () => { + const formGroup = service.createUnitTypeFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-type/update/unit-type-form.service.ts b/src/main/webapp/app/entities/unit-type/update/unit-type-form.service.ts new file mode 100644 index 0000000..615e552 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/update/unit-type-form.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IUnitType, NewUnitType } from '../unit-type.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IUnitType for edit and NewUnitTypeFormGroupInput for create. + */ +type UnitTypeFormGroupInput = IUnitType | PartialWithRequiredKeyOf; + +type UnitTypeFormDefaults = Pick; + +type UnitTypeFormGroupContent = { + id: FormControl; + name: FormControl; + description: FormControl; + valueType: FormControl; + version: FormControl; +}; + +export type UnitTypeFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class UnitTypeFormService { + createUnitTypeFormGroup(unitType: UnitTypeFormGroupInput = { id: null }): UnitTypeFormGroup { + const unitTypeRawValue = { + ...this.getFormDefaults(), + ...unitType, + }; + return new FormGroup({ + id: new FormControl( + { value: unitTypeRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + name: new FormControl(unitTypeRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(unitTypeRawValue.description, { + validators: [Validators.required], + }), + valueType: new FormControl(unitTypeRawValue.valueType, { + validators: [Validators.required], + }), + version: new FormControl(unitTypeRawValue.version), + }); + } + + getUnitType(form: UnitTypeFormGroup): IUnitType | NewUnitType { + return form.getRawValue() as IUnitType | NewUnitType; + } + + resetForm(form: UnitTypeFormGroup, unitType: UnitTypeFormGroupInput): void { + const unitTypeRawValue = { ...this.getFormDefaults(), ...unitType }; + form.reset( + { + ...unitTypeRawValue, + id: { value: unitTypeRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): UnitTypeFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.html b/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.html new file mode 100644 index 0000000..8960294 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.html @@ -0,0 +1,81 @@ + +
+
+
+

+ Criar ou editar Unit Type +

+ +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('valueType')!.invalid && (editForm.get('valueType')!.dirty || editForm.get('valueType')!.touched)) { +
+ @if (editForm.get('valueType')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ +
+ + + @if (editForm.controls.id.value !== null) { + } + +
+
+
+
diff --git a/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.spec.ts b/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.spec.ts new file mode 100644 index 0000000..be71548 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { UnitTypeService } from '../service/unit-type.service'; +import { IUnitType } from '../unit-type.model'; +import { UnitTypeFormService } from './unit-type-form.service'; + +import { UnitTypeUpdateComponent } from './unit-type-update.component'; + +describe('UnitType Management Update Component', () => { + let comp: UnitTypeUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let unitTypeFormService: UnitTypeFormService; + let unitTypeService: UnitTypeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitTypeUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(UnitTypeUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(UnitTypeUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + unitTypeFormService = TestBed.inject(UnitTypeFormService); + unitTypeService = TestBed.inject(UnitTypeService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should update editForm', () => { + const unitType: IUnitType = { id: 456 }; + + activatedRoute.data = of({ unitType }); + comp.ngOnInit(); + + expect(comp.unitType).toEqual(unitType); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const unitType = { id: 123 }; + jest.spyOn(unitTypeFormService, 'getUnitType').mockReturnValue(unitType); + jest.spyOn(unitTypeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unitType }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: unitType })); + saveSubject.complete(); + + // THEN + expect(unitTypeFormService.getUnitType).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(unitTypeService.update).toHaveBeenCalledWith(expect.objectContaining(unitType)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const unitType = { id: 123 }; + jest.spyOn(unitTypeFormService, 'getUnitType').mockReturnValue({ id: null }); + jest.spyOn(unitTypeService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unitType: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: unitType })); + saveSubject.complete(); + + // THEN + expect(unitTypeFormService.getUnitType).toHaveBeenCalled(); + expect(unitTypeService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const unitType = { id: 123 }; + jest.spyOn(unitTypeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unitType }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(unitTypeService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.ts b/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.ts new file mode 100644 index 0000000..e70d452 --- /dev/null +++ b/src/main/webapp/app/entities/unit-type/update/unit-type-update.component.ts @@ -0,0 +1,81 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { ResilientBaseComponent } from 'app/resilient/resilient-base/resilient-base.component'; + +import { UnitValueType } from 'app/entities/enumerations/unit-value-type.model'; +import { IUnitType } from '../unit-type.model'; +import { UnitTypeService } from '../service/unit-type.service'; +import { UnitTypeFormService, UnitTypeFormGroup } from './unit-type-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-unit-type-update', + templateUrl: './unit-type-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class UnitTypeUpdateComponent extends ResilientBaseComponent implements OnInit { + isSaving = false; + unitType: IUnitType | null = null; + unitValueTypeValues = Object.keys(UnitValueType); + + protected unitTypeService = inject(UnitTypeService); + protected unitTypeFormService = inject(UnitTypeFormService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: UnitTypeFormGroup = this.unitTypeFormService.createUnitTypeFormGroup(); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ unitType }) => { + this.unitType = unitType; + if (unitType) { + this.updateForm(unitType); + } + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const unitType = this.unitTypeFormService.getUnitType(this.editForm); + if (unitType.id !== null) { + this.subscribeToSaveResponse(this.unitTypeService.update(unitType)); + } else { + this.subscribeToSaveResponse(this.unitTypeService.create(unitType)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(unitType: IUnitType): void { + this.unitType = unitType; + this.unitTypeFormService.resetForm(this.editForm, unitType); + } +} diff --git a/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.html b/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.html new file mode 100644 index 0000000..e8c6763 --- /dev/null +++ b/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (unit) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.spec.ts new file mode 100644 index 0000000..935715d --- /dev/null +++ b/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { UnitService } from '../service/unit.service'; + +import { UnitDeleteDialogComponent } from './unit-delete-dialog.component'; + +describe('Unit Management Delete Component', () => { + let comp: UnitDeleteDialogComponent; + let fixture: ComponentFixture; + let service: UnitService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(UnitDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(UnitDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UnitService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.ts b/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.ts new file mode 100644 index 0000000..542556b --- /dev/null +++ b/src/main/webapp/app/entities/unit/delete/unit-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IUnit } from '../unit.model'; +import { UnitService } from '../service/unit.service'; + +@Component({ + standalone: true, + templateUrl: './unit-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class UnitDeleteDialogComponent { + unit?: IUnit; + + protected unitService = inject(UnitService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.unitService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/unit/detail/unit-detail.component.html b/src/main/webapp/app/entities/unit/detail/unit-detail.component.html new file mode 100644 index 0000000..d3dd76b --- /dev/null +++ b/src/main/webapp/app/entities/unit/detail/unit-detail.component.html @@ -0,0 +1,71 @@ + +
+
+ @if (unit()) { +
+

Unit

+ +
+ + + + + +
+
Unit Type
+
+ @if (unit()!.unitType) { + + } +
+
Code
+
+ {{ unit()!.code }} +
+
Symbol
+
+ {{ unit()!.symbol }} +
+
Name
+
+ {{ unit()!.name }} +
+
Description
+
+ {{ unit()!.description }} +
+
+ Convertion Rate +
+
+ {{ unit()!.convertionRate }} +
+ + + +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/unit/detail/unit-detail.component.html.old b/src/main/webapp/app/entities/unit/detail/unit-detail.component.html.old new file mode 100644 index 0000000..d0b9c78 --- /dev/null +++ b/src/main/webapp/app/entities/unit/detail/unit-detail.component.html.old @@ -0,0 +1,66 @@ +
+
+ @if (unit()) { +
+

Unit

+ +
+ + + + + +
+
Código
+
+ {{ unit()!.id }} +
+
Code
+
+ {{ unit()!.code }} +
+
Symbol
+
+ {{ unit()!.symbol }} +
+
Name
+
+ {{ unit()!.name }} +
+
Description
+
+ {{ unit()!.description }} +
+
+ Convertion Rate +
+
+ {{ unit()!.convertionRate }} +
+
Version
+
+ {{ unit()!.version }} +
+
Unit Type
+
+ @if (unit()!.unitType) { + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/unit/detail/unit-detail.component.spec.ts b/src/main/webapp/app/entities/unit/detail/unit-detail.component.spec.ts new file mode 100644 index 0000000..2fea749 --- /dev/null +++ b/src/main/webapp/app/entities/unit/detail/unit-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { UnitDetailComponent } from './unit-detail.component'; + +describe('Unit Management Detail Component', () => { + let comp: UnitDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UnitDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: UnitDetailComponent, + resolve: { unit: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(UnitDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UnitDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load unit on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', UnitDetailComponent); + + // THEN + expect(instance.unit()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit/detail/unit-detail.component.ts b/src/main/webapp/app/entities/unit/detail/unit-detail.component.ts new file mode 100644 index 0000000..a1a12ae --- /dev/null +++ b/src/main/webapp/app/entities/unit/detail/unit-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IUnit } from '../unit.model'; + +@Component({ + standalone: true, + selector: 'jhi-unit-detail', + templateUrl: './unit-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class UnitDetailComponent { + unit = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/unit/list/unit-search-form.service.spec.ts b/src/main/webapp/app/entities/unit/list/unit-search-form.service.spec.ts new file mode 100644 index 0000000..ac24c9a --- /dev/null +++ b/src/main/webapp/app/entities/unit/list/unit-search-form.service.spec.ts @@ -0,0 +1,96 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../unit.test-samples'; + +import { UnitSearchFormService } from './unit-search-form.service'; + +describe('Unit Search Form Service', () => { + let service: UnitSearchFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UnitSearchFormService); + }); + + describe('Service methods', () => { + describe('createUnitsearchFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createUnitSearchFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + symbol: expect.any(Object), + description: expect.any(Object), + version: expect.any(Object), + unitTypeId: expect.any(Object), + }), + ); + }); + + it('passing SearchUnit should create a new form with FormGroup', () => { + const formGroup = service.createUnitSearchFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + symbol: expect.any(Object), + description: expect.any(Object), + version: expect.any(Object), + unitTypeId: expect.any(Object), + }), + ); + }); + }); + + describe('getUnit', () => { + it('should return SearchUnit for default Unit initial value', () => { + const formGroup = service.createUnitSearchFormGroup(sampleWithNewData); + + const unit = service.getUnit(formGroup) as any; + + expect(unit).toMatchObject(sampleWithNewData); + }); + + it('should return SearchUnit for empty Unit initial value', () => { + const formGroup = service.createUnitSearchFormGroup(); + + const unit = service.getUnit(formGroup) as any; + + expect(unit).toMatchObject({}); + }); + + it('should return SearchUnit', () => { + const formGroup = service.createUnitSearchFormGroup(sampleWithRequiredData); + + const unit = service.getUnit(formGroup) as any; + + expect(unit).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IUnit should not enable id FormControl', () => { + const formGroup = service.createUnitSearchFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing SearchUnit should disable id FormControl', () => { + const formGroup = service.createUnitSearchFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit/list/unit-search-form.service.ts b/src/main/webapp/app/entities/unit/list/unit-search-form.service.ts new file mode 100644 index 0000000..8f71af4 --- /dev/null +++ b/src/main/webapp/app/entities/unit/list/unit-search-form.service.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import dayjs from 'dayjs/esm'; +import { DATE_TIME_FORMAT } from 'app/config/input.constants'; +import { SearchUnit } from '../unit.model'; +import { SearchFormService } from 'app/entities/shared/shared-search-form.service'; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts SearchUnit, for search. in the list + */ +type UnitSearchFormGroupInput = SearchUnit; + +type UnitSearchFormDefaults = SearchUnit; + +type UnitSearchFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + symbol: FormControl; + description: FormControl; + version: FormControl; + unitTypeId: FormControl; +}; + +export type UnitSearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class UnitSearchFormService extends SearchFormService { + createUnitSearchFormGroup(unit: UnitSearchFormGroupInput = { id: null }): UnitSearchFormGroup { + const unitRawValue = { + ...this.getSearchFormDefaults(), + ...unit, + }; + return new FormGroup({ + id: new FormControl(unitRawValue.id), + code: new FormControl(unitRawValue.code), + name: new FormControl(unitRawValue.name), + symbol: new FormControl(unitRawValue.symbol), + description: new FormControl(unitRawValue.description), + version: new FormControl(unitRawValue.version), + unitTypeId: new FormControl(unitRawValue.unitTypeId), + }); + } + + getUnit(form: UnitSearchFormGroup): SearchUnit { + return form.getRawValue() as SearchUnit; + } + + resetForm(form: UnitSearchFormGroup, unit: UnitSearchFormGroupInput): void { + const unitRawValue = { ...this.getSearchFormDefaults(), ...unit }; + form.reset( + { + ...unitRawValue, + id: { value: unitRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): UnitSearchFormDefaults { + const currentTime = dayjs(); + + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/unit/list/unit.component.html b/src/main/webapp/app/entities/unit/list/unit.component.html new file mode 100644 index 0000000..57ad2ae --- /dev/null +++ b/src/main/webapp/app/entities/unit/list/unit.component.html @@ -0,0 +1,152 @@ + +
+

+ Units + +
+ + + +
+

+ + + + + + @if (units && units.length >= 0) { +
+ {{ ' ' // Wrap the all table in a form. This is the only way reactiveforms will work }} +
+ + + + {{ ' ' // Header row }} + + + + + + + + + + {{ ' ' // Search row }} + + + + + + + + + + @for (unit of units; track trackId) { + + + + + + + + + + + } + +
+
+ Unit Type + +
+
+
+ Symbol + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+ @if (unit.unitType) { + + } + {{ unit.symbol }}{{ unit.name }}{{ unit.description }} +
+ + + Visualizar + + + + + Editar + + + +
+
+ +
+
+ } + + @if (units?.length === 0) { +
+ Nenhum Units encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/unit/list/unit.component.spec.ts b/src/main/webapp/app/entities/unit/list/unit.component.spec.ts new file mode 100644 index 0000000..65683ad --- /dev/null +++ b/src/main/webapp/app/entities/unit/list/unit.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../unit.test-samples'; +import { UnitService } from '../service/unit.service'; + +import { UnitComponent } from './unit.component'; +import SpyInstance = jest.SpyInstance; + +describe('Unit Management Component', () => { + let comp: UnitComponent; + let fixture: ComponentFixture; + let service: UnitService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(UnitComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(UnitComponent); + comp = fixture.componentInstance; + service = TestBed.inject(UnitService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.units?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to unitService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getUnitIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getUnitIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/unit/list/unit.component.ts b/src/main/webapp/app/entities/unit/list/unit.component.ts new file mode 100644 index 0000000..6f51d6b --- /dev/null +++ b/src/main/webapp/app/entities/unit/list/unit.component.ts @@ -0,0 +1,153 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, map } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IUnit } from '../unit.model'; +import { EntityArrayResponseType, UnitService } from '../service/unit.service'; +import { UnitDeleteDialogComponent } from '../delete/unit-delete-dialog.component'; + +import { UnitTypeService } from 'app/entities/unit-type/service/unit-type.service'; +import { IUnitType } from 'app/entities/unit-type/unit-type.model'; +import { UnitSearchFormService, UnitSearchFormGroup } from './unit-search-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-unit', + templateUrl: './unit.component.html', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class UnitComponent implements OnInit { + subscription: Subscription | null = null; + units?: IUnit[]; + isLoading = false; + + sortState = sortStateSignal({}); + + unitTypesSharedCollection: IUnitType[] = []; + + public router = inject(Router); + protected unitTypeService = inject(UnitTypeService); + protected unitService = inject(UnitService); + protected unitSearchFormService = inject(UnitSearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + searchForm: UnitSearchFormGroup = this.unitSearchFormService.createUnitSearchFormGroup(); + trackId = (_index: number, item: IUnit): number => this.unitService.getEntityIdentifier(item); + + compareUnitType = (o1: IUnitType | null, o2: IUnitType | null): boolean => + this.unitTypeService.compareEntity(o1, o2); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.units || this.units.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + + this.loadRelationshipsOptions(); + } + + delete(unit: IUnit): void { + const modalRef = this.modalService.open(UnitDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.unit = unit; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + onSearch(): void { + this.load(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.units = this.refineData(dataFromBody); + } + + protected refineData(data: IUnit[]): IUnit[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IUnit[] | null): IUnit[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + + const searchModel = this.unitSearchFormService.getUnit(this.searchForm); + this.unitSearchFormService.applySearchConditionsToQueryObject(queryObject, searchModel); + + return this.unitService.queryByCriteria(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } + + protected loadRelationshipsOptions(): void { + this.unitTypeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((unitTypes: IUnitType[]) => (this.unitTypesSharedCollection = unitTypes)); + } +} diff --git a/src/main/webapp/app/entities/unit/route/unit-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/unit/route/unit-routing-resolve.service.spec.ts new file mode 100644 index 0000000..bc7bbe0 --- /dev/null +++ b/src/main/webapp/app/entities/unit/route/unit-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IUnit } from '../unit.model'; +import { UnitService } from '../service/unit.service'; + +import unitResolve from './unit-routing-resolve.service'; + +describe('Unit routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: UnitService; + let resultUnit: IUnit | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(UnitService); + resultUnit = undefined; + }); + + describe('resolve', () => { + it('should return IUnit returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + unitResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnit = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultUnit).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + unitResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnit = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultUnit).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + unitResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultUnit = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultUnit).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit/route/unit-routing-resolve.service.ts b/src/main/webapp/app/entities/unit/route/unit-routing-resolve.service.ts new file mode 100644 index 0000000..30ba621 --- /dev/null +++ b/src/main/webapp/app/entities/unit/route/unit-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IUnit } from '../unit.model'; +import { UnitService } from '../service/unit.service'; + +const unitResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(UnitService) + .find(id) + .pipe( + mergeMap((unit: HttpResponse) => { + if (unit.body) { + return of(unit.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default unitResolve; diff --git a/src/main/webapp/app/entities/unit/service/unit.service.spec.ts b/src/main/webapp/app/entities/unit/service/unit.service.spec.ts new file mode 100644 index 0000000..fed548a --- /dev/null +++ b/src/main/webapp/app/entities/unit/service/unit.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IUnit } from '../unit.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../unit.test-samples'; + +import { UnitService } from './unit.service'; + +const requireRestSample: IUnit = { + ...sampleWithRequiredData, +}; + +describe('Unit Service', () => { + let service: UnitService; + let httpMock: HttpTestingController; + let expectedResult: IUnit | IUnit[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(UnitService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a Unit', () => { + const unit = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(unit).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a Unit', () => { + const unit = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(unit).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a Unit', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of Unit', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a Unit', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addUnitToCollectionIfMissing', () => { + it('should add a Unit to an empty array', () => { + const unit: IUnit = sampleWithRequiredData; + expectedResult = service.addUnitToCollectionIfMissing([], unit); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(unit); + }); + + it('should not add a Unit to an array that contains it', () => { + const unit: IUnit = sampleWithRequiredData; + const unitCollection: IUnit[] = [ + { + ...unit, + }, + sampleWithPartialData, + ]; + expectedResult = service.addUnitToCollectionIfMissing(unitCollection, unit); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a Unit to an array that doesn't contain it", () => { + const unit: IUnit = sampleWithRequiredData; + const unitCollection: IUnit[] = [sampleWithPartialData]; + expectedResult = service.addUnitToCollectionIfMissing(unitCollection, unit); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(unit); + }); + + it('should add only unique Unit to an array', () => { + const unitArray: IUnit[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const unitCollection: IUnit[] = [sampleWithRequiredData]; + expectedResult = service.addUnitToCollectionIfMissing(unitCollection, ...unitArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const unit: IUnit = sampleWithRequiredData; + const unit2: IUnit = sampleWithPartialData; + expectedResult = service.addUnitToCollectionIfMissing([], unit, unit2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(unit); + expect(expectedResult).toContain(unit2); + }); + + it('should accept null and undefined values', () => { + const unit: IUnit = sampleWithRequiredData; + expectedResult = service.addUnitToCollectionIfMissing([], null, unit, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(unit); + }); + + it('should return initial array if no Unit is added', () => { + const unitCollection: IUnit[] = [sampleWithRequiredData]; + expectedResult = service.addUnitToCollectionIfMissing(unitCollection, undefined, null); + expect(expectedResult).toEqual(unitCollection); + }); + }); + + describe('compareUnit', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareUnit(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareUnit(entity1, entity2); + const compareResult2 = service.compareUnit(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareUnit(entity1, entity2); + const compareResult2 = service.compareUnit(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareUnit(entity1, entity2); + const compareResult2 = service.compareUnit(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/unit/service/unit.service.ts b/src/main/webapp/app/entities/unit/service/unit.service.ts new file mode 100644 index 0000000..5eafb46 --- /dev/null +++ b/src/main/webapp/app/entities/unit/service/unit.service.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IUnit, NewUnit } from '../unit.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class UnitService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/units'); + getResourceUrl(): string { return this.resourceUrl; } + + queryByType(unitTypeId: number): Observable { + return this.http.get(`${this.resourceUrl}/byType/${unitTypeId}`, { observe: 'response' }); + } + + queryForVariable(variableId: number): Observable { + return this.http.get(`${this.resourceUrl}/forVariable/${variableId}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/unit/unit.model.ts b/src/main/webapp/app/entities/unit/unit.model.ts new file mode 100644 index 0000000..6b75046 --- /dev/null +++ b/src/main/webapp/app/entities/unit/unit.model.ts @@ -0,0 +1,25 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IUnitType } from 'app/entities/unit-type/unit-type.model'; + +export interface IUnit extends IResilientEntity { + code?: string | null; + symbol?: string | null; + name?: string | null; + description?: string | null; + convertionRate?: number | null; + unitType?: Pick | null; +} + +export type NewUnit = Omit & { id: null }; + +export type SearchUnit = Omit< + IUnit, + // Remove unwanted properties for search + 'id' | 'convertionRate' | 'unitType'> + & + // Add specific properties for search + { + id?: number | null; + unitTypeId?: number | null; + } diff --git a/src/main/webapp/app/entities/unit/unit.routes.ts b/src/main/webapp/app/entities/unit/unit.routes.ts new file mode 100644 index 0000000..5b95edb --- /dev/null +++ b/src/main/webapp/app/entities/unit/unit.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { UnitComponent } from './list/unit.component'; +import { UnitDetailComponent } from './detail/unit-detail.component'; +import { UnitUpdateComponent } from './update/unit-update.component'; +import UnitResolve from './route/unit-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { UnitService } from './service/unit.service'; +import { Resources } from 'app/security/resources.model'; + +const unitRoute: Routes = [ + { + path: '', + component: UnitComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: UnitService, + resource: Resources.UNIT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: UnitDetailComponent, + resolve: { + unit: UnitResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: UnitService, + resource: Resources.UNIT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: UnitUpdateComponent, + resolve: { + unit: UnitResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: UnitService, + resource: Resources.UNIT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: UnitUpdateComponent, + resolve: { + unit: UnitResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: UnitService, + resource: Resources.UNIT, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default unitRoute; diff --git a/src/main/webapp/app/entities/unit/unit.test-samples.ts b/src/main/webapp/app/entities/unit/unit.test-samples.ts new file mode 100644 index 0000000..13f7598 --- /dev/null +++ b/src/main/webapp/app/entities/unit/unit.test-samples.ts @@ -0,0 +1,40 @@ +import { IUnit, NewUnit } from './unit.model'; + +export const sampleWithRequiredData: IUnit = { + id: 16295, + code: 'colorfully chop whoa', + symbol: 'polish', + name: 'geez nutritious', + convertionRate: 8284.61, +}; + +export const sampleWithPartialData: IUnit = { + id: 23265, + code: 'slowly while proofread', + symbol: 'quarry carefully', + name: 'which gadzooks forenenst', + convertionRate: 6539.49, +}; + +export const sampleWithFullData: IUnit = { + id: 13097, + code: 'handsome', + symbol: 'rush angle how', + name: 'finally delightfully', + description: 'gain', + convertionRate: 28595.26, + version: 26045, +}; + +export const sampleWithNewData: NewUnit = { + code: 'likely meanwhile ouch', + symbol: 'of', + name: 'shadowbox muted huzzah', + convertionRate: 22765.5, + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/unit/update/unit-form.service.spec.ts b/src/main/webapp/app/entities/unit/update/unit-form.service.spec.ts new file mode 100644 index 0000000..3ea5c34 --- /dev/null +++ b/src/main/webapp/app/entities/unit/update/unit-form.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../unit.test-samples'; + +import { UnitFormService } from './unit-form.service'; + +describe('Unit Form Service', () => { + let service: UnitFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UnitFormService); + }); + + describe('Service methods', () => { + describe('createUnitFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createUnitFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + symbol: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + convertionRate: expect.any(Object), + version: expect.any(Object), + unitType: expect.any(Object), + }), + ); + }); + + it('passing IUnit should create a new form with FormGroup', () => { + const formGroup = service.createUnitFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + symbol: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + convertionRate: expect.any(Object), + version: expect.any(Object), + unitType: expect.any(Object), + }), + ); + }); + }); + + describe('getUnit', () => { + it('should return NewUnit for default Unit initial value', () => { + const formGroup = service.createUnitFormGroup(sampleWithNewData); + + const unit = service.getUnit(formGroup) as any; + + expect(unit).toMatchObject(sampleWithNewData); + }); + + it('should return NewUnit for empty Unit initial value', () => { + const formGroup = service.createUnitFormGroup(); + + const unit = service.getUnit(formGroup) as any; + + expect(unit).toMatchObject({}); + }); + + it('should return IUnit', () => { + const formGroup = service.createUnitFormGroup(sampleWithRequiredData); + + const unit = service.getUnit(formGroup) as any; + + expect(unit).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IUnit should not enable id FormControl', () => { + const formGroup = service.createUnitFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewUnit should disable id FormControl', () => { + const formGroup = service.createUnitFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit/update/unit-form.service.ts b/src/main/webapp/app/entities/unit/update/unit-form.service.ts new file mode 100644 index 0000000..e2d6ead --- /dev/null +++ b/src/main/webapp/app/entities/unit/update/unit-form.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IUnit, NewUnit } from '../unit.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IUnit for edit and NewUnitFormGroupInput for create. + */ +type UnitFormGroupInput = IUnit | PartialWithRequiredKeyOf; + +type UnitFormDefaults = Pick; + +type UnitFormGroupContent = { + id: FormControl; + code: FormControl; + symbol: FormControl; + name: FormControl; + description: FormControl; + convertionRate: FormControl; + version: FormControl; + unitType: FormControl; +}; + +export type UnitFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class UnitFormService { + createUnitFormGroup(unit: UnitFormGroupInput = { id: null }): UnitFormGroup { + const unitRawValue = { + ...this.getFormDefaults(), + ...unit, + }; + return new FormGroup({ + id: new FormControl( + { value: unitRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(unitRawValue.code, { + validators: [Validators.required], + }), + symbol: new FormControl(unitRawValue.symbol, { + validators: [Validators.required], + }), + name: new FormControl(unitRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(unitRawValue.description), + convertionRate: new FormControl(unitRawValue.convertionRate, { + validators: [Validators.required], + }), + version: new FormControl(unitRawValue.version), + unitType: new FormControl(unitRawValue.unitType), + }); + } + + getUnit(form: UnitFormGroup): IUnit | NewUnit { + return form.getRawValue() as IUnit | NewUnit; + } + + resetForm(form: UnitFormGroup, unit: UnitFormGroupInput): void { + const unitRawValue = { ...this.getFormDefaults(), ...unit }; + form.reset( + { + ...unitRawValue, + id: { value: unitRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): UnitFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/unit/update/unit-update.component.html b/src/main/webapp/app/entities/unit/update/unit-update.component.html new file mode 100644 index 0000000..7444c47 --- /dev/null +++ b/src/main/webapp/app/entities/unit/update/unit-update.component.html @@ -0,0 +1,166 @@ + +
+
+
+

+ Criar ou editar Unit +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + +
+
+ + + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('symbol')!.invalid && (editForm.get('symbol')!.dirty || editForm.get('symbol')!.touched)) { +
+ @if (editForm.get('symbol')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + +
+
+ + + @if ( + editForm.get('convertionRate')!.invalid && (editForm.get('convertionRate')!.dirty || editForm.get('convertionRate')!.touched) + ) { +
+ @if (editForm.get('convertionRate')?.errors?.required) { + O campo é obrigatório. + } + Este campo é do tipo número. +
+ } +
+ + + +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/unit/update/unit-update.component.html.old b/src/main/webapp/app/entities/unit/update/unit-update.component.html.old new file mode 100644 index 0000000..452d64f --- /dev/null +++ b/src/main/webapp/app/entities/unit/update/unit-update.component.html.old @@ -0,0 +1,163 @@ +
+
+
+

+ Criar ou editar Unit +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('symbol')!.invalid && (editForm.get('symbol')!.dirty || editForm.get('symbol')!.touched)) { +
+ @if (editForm.get('symbol')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+ +
+ + + @if ( + editForm.get('convertionRate')!.invalid && (editForm.get('convertionRate')!.dirty || editForm.get('convertionRate')!.touched) + ) { +
+ @if (editForm.get('convertionRate')?.errors?.required) { + O campo é obrigatório. + } + Este campo é do tipo número. +
+ } +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/unit/update/unit-update.component.spec.ts b/src/main/webapp/app/entities/unit/update/unit-update.component.spec.ts new file mode 100644 index 0000000..ecf90e5 --- /dev/null +++ b/src/main/webapp/app/entities/unit/update/unit-update.component.spec.ts @@ -0,0 +1,164 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IUnitType } from 'app/entities/unit-type/unit-type.model'; +import { UnitTypeService } from 'app/entities/unit-type/service/unit-type.service'; +import { UnitService } from '../service/unit.service'; +import { IUnit } from '../unit.model'; +import { UnitFormService } from './unit-form.service'; + +import { UnitUpdateComponent } from './unit-update.component'; + +describe('Unit Management Update Component', () => { + let comp: UnitUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let unitFormService: UnitFormService; + let unitService: UnitService; + let unitTypeService: UnitTypeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, UnitUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(UnitUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(UnitUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + unitFormService = TestBed.inject(UnitFormService); + unitService = TestBed.inject(UnitService); + unitTypeService = TestBed.inject(UnitTypeService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call UnitType query and add missing value', () => { + const unit: IUnit = { id: 456 }; + const unitType: IUnitType = { id: 19389 }; + unit.unitType = unitType; + + const unitTypeCollection: IUnitType[] = [{ id: 20356 }]; + jest.spyOn(unitTypeService, 'query').mockReturnValue(of(new HttpResponse({ body: unitTypeCollection }))); + const additionalUnitTypes = [unitType]; + const expectedCollection: IUnitType[] = [...additionalUnitTypes, ...unitTypeCollection]; + jest.spyOn(unitTypeService, 'addUnitTypeToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ unit }); + comp.ngOnInit(); + + expect(unitTypeService.query).toHaveBeenCalled(); + expect(unitTypeService.addUnitTypeToCollectionIfMissing).toHaveBeenCalledWith( + unitTypeCollection, + ...additionalUnitTypes.map(expect.objectContaining), + ); + expect(comp.unitTypesSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const unit: IUnit = { id: 456 }; + const unitType: IUnitType = { id: 6897 }; + unit.unitType = unitType; + + activatedRoute.data = of({ unit }); + comp.ngOnInit(); + + expect(comp.unitTypesSharedCollection).toContain(unitType); + expect(comp.unit).toEqual(unit); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const unit = { id: 123 }; + jest.spyOn(unitFormService, 'getUnit').mockReturnValue(unit); + jest.spyOn(unitService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unit }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: unit })); + saveSubject.complete(); + + // THEN + expect(unitFormService.getUnit).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(unitService.update).toHaveBeenCalledWith(expect.objectContaining(unit)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const unit = { id: 123 }; + jest.spyOn(unitFormService, 'getUnit').mockReturnValue({ id: null }); + jest.spyOn(unitService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unit: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: unit })); + saveSubject.complete(); + + // THEN + expect(unitFormService.getUnit).toHaveBeenCalled(); + expect(unitService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const unit = { id: 123 }; + jest.spyOn(unitService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ unit }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(unitService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareUnitType', () => { + it('Should forward to unitTypeService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(unitTypeService, 'compareUnitType'); + comp.compareUnitType(entity, entity2); + expect(unitTypeService.compareUnitType).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/unit/update/unit-update.component.ts b/src/main/webapp/app/entities/unit/update/unit-update.component.ts new file mode 100644 index 0000000..71f15e0 --- /dev/null +++ b/src/main/webapp/app/entities/unit/update/unit-update.component.ts @@ -0,0 +1,101 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IUnitType } from 'app/entities/unit-type/unit-type.model'; +import { UnitTypeService } from 'app/entities/unit-type/service/unit-type.service'; +import { IUnit } from '../unit.model'; +import { UnitService } from '../service/unit.service'; +import { UnitFormService, UnitFormGroup } from './unit-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-unit-update', + templateUrl: './unit-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class UnitUpdateComponent implements OnInit { + isSaving = false; + unit: IUnit | null = null; + + unitTypesSharedCollection: IUnitType[] = []; + + protected unitService = inject(UnitService); + protected unitFormService = inject(UnitFormService); + protected unitTypeService = inject(UnitTypeService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: UnitFormGroup = this.unitFormService.createUnitFormGroup(); + + compareUnitType = (o1: IUnitType | null, o2: IUnitType | null): boolean => this.unitTypeService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ unit }) => { + this.unit = unit; + if (unit) { + this.updateForm(unit); + } + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const unit = this.unitFormService.getUnit(this.editForm); + if (unit.id !== null) { + this.subscribeToSaveResponse(this.unitService.update(unit)); + } else { + this.subscribeToSaveResponse(this.unitService.create(unit)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(unit: IUnit): void { + this.unit = unit; + this.unitFormService.resetForm(this.editForm, unit); + + this.unitTypesSharedCollection = this.unitTypeService.addEntityToCollectionIfMissing( + this.unitTypesSharedCollection, + unit.unitType, + ); + } + + protected loadRelationshipsOptions(): void { + this.unitTypeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((unitTypes: IUnitType[]) => this.unitTypeService.addEntityToCollectionIfMissing(unitTypes, this.unit?.unitType)), + ) + .subscribe((unitTypes: IUnitType[]) => (this.unitTypesSharedCollection = unitTypes)); + } +} diff --git a/src/main/webapp/app/entities/user/service/user.service.spec.ts b/src/main/webapp/app/entities/user/service/user.service.spec.ts new file mode 100644 index 0000000..59e426d --- /dev/null +++ b/src/main/webapp/app/entities/user/service/user.service.spec.ts @@ -0,0 +1,158 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IUser } from '../user.model'; +import { sampleWithRequiredData, sampleWithPartialData, sampleWithFullData } from '../user.test-samples'; + +import { UserService } from './user.service'; + +const requireRestSample: IUser = { + ...sampleWithRequiredData, +}; + +describe('User Service', () => { + let service: UserService; + let httpMock: HttpTestingController; + let expectedResult: IUser | IUser[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(UserService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of User', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + describe('addUserToCollectionIfMissing', () => { + it('should add a User to an empty array', () => { + const user: IUser = sampleWithRequiredData; + expectedResult = service.addUserToCollectionIfMissing([], user); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(user); + }); + + it('should not add a User to an array that contains it', () => { + const user: IUser = sampleWithRequiredData; + const userCollection: IUser[] = [ + { + ...user, + }, + sampleWithPartialData, + ]; + expectedResult = service.addUserToCollectionIfMissing(userCollection, user); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a User to an array that doesn't contain it", () => { + const user: IUser = sampleWithRequiredData; + const userCollection: IUser[] = [sampleWithPartialData]; + expectedResult = service.addUserToCollectionIfMissing(userCollection, user); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(user); + }); + + it('should add only unique User to an array', () => { + const userArray: IUser[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const userCollection: IUser[] = [sampleWithRequiredData]; + expectedResult = service.addUserToCollectionIfMissing(userCollection, ...userArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const user: IUser = sampleWithRequiredData; + const user2: IUser = sampleWithPartialData; + expectedResult = service.addUserToCollectionIfMissing([], user, user2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(user); + expect(expectedResult).toContain(user2); + }); + + it('should accept null and undefined values', () => { + const user: IUser = sampleWithRequiredData; + expectedResult = service.addUserToCollectionIfMissing([], null, user, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(user); + }); + + it('should return initial array if no User is added', () => { + const userCollection: IUser[] = [sampleWithRequiredData]; + expectedResult = service.addUserToCollectionIfMissing(userCollection, undefined, null); + expect(expectedResult).toEqual(userCollection); + }); + }); + + describe('compareUser', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareUser(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareUser(entity1, entity2); + const compareResult2 = service.compareUser(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareUser(entity1, entity2); + const compareResult2 = service.compareUser(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareUser(entity1, entity2); + const compareResult2 = service.compareUser(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/user/service/user.service.ts b/src/main/webapp/app/entities/user/service/user.service.ts new file mode 100644 index 0000000..d801fd0 --- /dev/null +++ b/src/main/webapp/app/entities/user/service/user.service.ts @@ -0,0 +1,56 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IUser } from '../user.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class UserService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/users'); + + find(id: number): Observable { + return this.http.get(`${this.resourceUrl}/${id}`, { observe: 'response' }); + } + + query(req?: any): Observable { + const options = createRequestOption(req); + return this.http.get(this.resourceUrl, { params: options, observe: 'response' }); + } + + getUserIdentifier(user: Pick): number { + return user.id; + } + + compareUser(o1: Pick | null, o2: Pick | null): boolean { + return o1 && o2 ? this.getUserIdentifier(o1) === this.getUserIdentifier(o2) : o1 === o2; + } + + addUserToCollectionIfMissing>( + userCollection: Type[], + ...usersToCheck: (Type | null | undefined)[] + ): Type[] { + const users: Type[] = usersToCheck.filter(isPresent); + if (users.length > 0) { + const userCollectionIdentifiers = userCollection.map(userItem => this.getUserIdentifier(userItem)); + const usersToAdd = users.filter(userItem => { + const userIdentifier = this.getUserIdentifier(userItem); + if (userCollectionIdentifiers.includes(userIdentifier)) { + return false; + } + userCollectionIdentifiers.push(userIdentifier); + return true; + }); + return [...usersToAdd, ...userCollection]; + } + return userCollection; + } +} diff --git a/src/main/webapp/app/entities/user/user.model.ts b/src/main/webapp/app/entities/user/user.model.ts new file mode 100644 index 0000000..fc704a4 --- /dev/null +++ b/src/main/webapp/app/entities/user/user.model.ts @@ -0,0 +1,4 @@ +export interface IUser { + id: number; + login?: string | null; +} diff --git a/src/main/webapp/app/entities/user/user.test-samples.ts b/src/main/webapp/app/entities/user/user.test-samples.ts new file mode 100644 index 0000000..5236f36 --- /dev/null +++ b/src/main/webapp/app/entities/user/user.test-samples.ts @@ -0,0 +1,19 @@ +import { IUser } from './user.model'; + +export const sampleWithRequiredData: IUser = { + id: 6534, + login: '+@Eeth\\ft0J\\AWgW2K', +}; + +export const sampleWithPartialData: IUser = { + id: 20485, + login: 'gUp', +}; + +export const sampleWithFullData: IUser = { + id: 16191, + login: '3', +}; +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.html b/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.html new file mode 100644 index 0000000..c649b88 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (variableCategory) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.spec.ts new file mode 100644 index 0000000..f24a09d --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { VariableCategoryService } from '../service/variable-category.service'; + +import { VariableCategoryDeleteDialogComponent } from './variable-category-delete-dialog.component'; + +describe('VariableCategory Management Delete Component', () => { + let comp: VariableCategoryDeleteDialogComponent; + let fixture: ComponentFixture; + let service: VariableCategoryService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableCategoryDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(VariableCategoryDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(VariableCategoryDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableCategoryService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.ts b/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.ts new file mode 100644 index 0000000..990d483 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/delete/variable-category-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IVariableCategory } from '../variable-category.model'; +import { VariableCategoryService } from '../service/variable-category.service'; + +@Component({ + standalone: true, + templateUrl: './variable-category-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class VariableCategoryDeleteDialogComponent { + variableCategory?: IVariableCategory; + + protected variableCategoryService = inject(VariableCategoryService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.variableCategoryService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.html b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.html new file mode 100644 index 0000000..92d1fe6 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.html @@ -0,0 +1,70 @@ + +
+
+ @if (variableCategory()) { +
+

+ Variable Category +

+ +
+ + + + + +
+
Variable Scope
+
+ @if (variableCategory()!.variableScope) { + + } +
+
+ Code +
+
+ {{ variableCategory()!.code }} +
+
+ Name +
+
+ {{ variableCategory()!.name }} +
+
+ Description +
+
+ {{ variableCategory()!.description }} +
+
Hidden for Data ?
+
+ {{ variableCategory()!.hiddenForData }} +
+ +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.html.old b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.html.old new file mode 100644 index 0000000..d5c9ec1 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.html.old @@ -0,0 +1,78 @@ +
+
+ @if (variableCategory()) { +
+

+ Variable Category +

+ +
+ + + + + +
+
Código
+
+ {{ variableCategory()!.id }} +
+
+ Code +
+
+ {{ variableCategory()!.code }} +
+
+ Name +
+
+ {{ variableCategory()!.name }} +
+
+ Description +
+
+ {{ variableCategory()!.description }} +
+
+ Version +
+
+ {{ variableCategory()!.version }} +
+
Variable Scope
+
+ @if (variableCategory()!.variableScope) { + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.spec.ts b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.spec.ts new file mode 100644 index 0000000..4da7b01 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { VariableCategoryDetailComponent } from './variable-category-detail.component'; + +describe('VariableCategory Management Detail Component', () => { + let comp: VariableCategoryDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VariableCategoryDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: VariableCategoryDetailComponent, + resolve: { variableCategory: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(VariableCategoryDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VariableCategoryDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load variableCategory on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', VariableCategoryDetailComponent); + + // THEN + expect(instance.variableCategory()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.ts b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.ts new file mode 100644 index 0000000..3831326 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/detail/variable-category-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IVariableCategory } from '../variable-category.model'; + +@Component({ + standalone: true, + selector: 'jhi-variable-category-detail', + templateUrl: './variable-category-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class VariableCategoryDetailComponent { + variableCategory = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/variable-category/list/variable-category.component.html b/src/main/webapp/app/entities/variable-category/list/variable-category.component.html new file mode 100644 index 0000000..4b45d2a --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/list/variable-category.component.html @@ -0,0 +1,142 @@ + +
+

+ Variable Categories + +
+ + + +
+

+ + + + + + @if (variableCategories?.length === 0) { +
+ Nenhum Variable Categories encontrado +
+ } + + @if (variableCategories && variableCategories.length > 0) { +
+ + + + + + + + + + + + + + + @for (variableCategory of variableCategories; track trackId) { + + + + + + + + + + + } + +
+
+ Variable Scope + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+ @if (variableCategory.variableScope) { + + } + {{ variableCategory.code }}{{ variableCategory.name }}{{ variableCategory.description }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/variable-category/list/variable-category.component.html.old b/src/main/webapp/app/entities/variable-category/list/variable-category.component.html.old new file mode 100644 index 0000000..29ca91d --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/list/variable-category.component.html.old @@ -0,0 +1,133 @@ +
+

+ Variable Categories + +
+ + + +
+

+ + + + + + @if (variableCategories?.length === 0) { +
+ Nenhum Variable Categories encontrado +
+ } + + @if (variableCategories && variableCategories.length > 0) { +
+ + + + + + + + + + + + + + @for (variableCategory of variableCategories; track trackId) { + + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Version + + +
+
+
+ Variable Scope + +
+
+ {{ variableCategory.id }} + {{ variableCategory.code }}{{ variableCategory.name }}{{ variableCategory.description }}{{ variableCategory.version }} + @if (variableCategory.variableScope) { + + } + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/variable-category/list/variable-category.component.spec.ts b/src/main/webapp/app/entities/variable-category/list/variable-category.component.spec.ts new file mode 100644 index 0000000..4eec18e --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/list/variable-category.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../variable-category.test-samples'; +import { VariableCategoryService } from '../service/variable-category.service'; + +import { VariableCategoryComponent } from './variable-category.component'; +import SpyInstance = jest.SpyInstance; + +describe('VariableCategory Management Component', () => { + let comp: VariableCategoryComponent; + let fixture: ComponentFixture; + let service: VariableCategoryService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableCategoryComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(VariableCategoryComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableCategoryComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableCategoryService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.variableCategories?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to variableCategoryService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getVariableCategoryIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getVariableCategoryIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/variable-category/list/variable-category.component.ts b/src/main/webapp/app/entities/variable-category/list/variable-category.component.ts new file mode 100644 index 0000000..2498a98 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/list/variable-category.component.ts @@ -0,0 +1,122 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IVariableCategory } from '../variable-category.model'; +import { EntityArrayResponseType, VariableCategoryService } from '../service/variable-category.service'; +import { VariableCategoryDeleteDialogComponent } from '../delete/variable-category-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-variable-category', + templateUrl: './variable-category.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class VariableCategoryComponent implements OnInit { + subscription: Subscription | null = null; + variableCategories?: IVariableCategory[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected variableCategoryService = inject(VariableCategoryService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IVariableCategory): number => this.variableCategoryService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.variableCategories || this.variableCategories.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(variableCategory: IVariableCategory): void { + const modalRef = this.modalService.open(VariableCategoryDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.variableCategory = variableCategory; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.variableCategories = this.refineData(dataFromBody); + } + + protected refineData(data: IVariableCategory[]): IVariableCategory[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IVariableCategory[] | null): IVariableCategory[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.variableCategoryService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/variable-category/route/variable-category-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/variable-category/route/variable-category-routing-resolve.service.spec.ts new file mode 100644 index 0000000..002ed0c --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/route/variable-category-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IVariableCategory } from '../variable-category.model'; +import { VariableCategoryService } from '../service/variable-category.service'; + +import variableCategoryResolve from './variable-category-routing-resolve.service'; + +describe('VariableCategory routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: VariableCategoryService; + let resultVariableCategory: IVariableCategory | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(VariableCategoryService); + resultVariableCategory = undefined; + }); + + describe('resolve', () => { + it('should return IVariableCategory returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableCategoryResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableCategory = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariableCategory).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + variableCategoryResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableCategory = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultVariableCategory).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableCategoryResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableCategory = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariableCategory).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-category/route/variable-category-routing-resolve.service.ts b/src/main/webapp/app/entities/variable-category/route/variable-category-routing-resolve.service.ts new file mode 100644 index 0000000..4a57312 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/route/variable-category-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IVariableCategory } from '../variable-category.model'; +import { VariableCategoryService } from '../service/variable-category.service'; + +const variableCategoryResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(VariableCategoryService) + .find(id) + .pipe( + mergeMap((variableCategory: HttpResponse) => { + if (variableCategory.body) { + return of(variableCategory.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default variableCategoryResolve; diff --git a/src/main/webapp/app/entities/variable-category/service/variable-category.service.spec.ts b/src/main/webapp/app/entities/variable-category/service/variable-category.service.spec.ts new file mode 100644 index 0000000..a003535 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/service/variable-category.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IVariableCategory } from '../variable-category.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../variable-category.test-samples'; + +import { VariableCategoryService } from './variable-category.service'; + +const requireRestSample: IVariableCategory = { + ...sampleWithRequiredData, +}; + +describe('VariableCategory Service', () => { + let service: VariableCategoryService; + let httpMock: HttpTestingController; + let expectedResult: IVariableCategory | IVariableCategory[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(VariableCategoryService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a VariableCategory', () => { + const variableCategory = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(variableCategory).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a VariableCategory', () => { + const variableCategory = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(variableCategory).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a VariableCategory', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of VariableCategory', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a VariableCategory', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addVariableCategoryToCollectionIfMissing', () => { + it('should add a VariableCategory to an empty array', () => { + const variableCategory: IVariableCategory = sampleWithRequiredData; + expectedResult = service.addVariableCategoryToCollectionIfMissing([], variableCategory); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variableCategory); + }); + + it('should not add a VariableCategory to an array that contains it', () => { + const variableCategory: IVariableCategory = sampleWithRequiredData; + const variableCategoryCollection: IVariableCategory[] = [ + { + ...variableCategory, + }, + sampleWithPartialData, + ]; + expectedResult = service.addVariableCategoryToCollectionIfMissing(variableCategoryCollection, variableCategory); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a VariableCategory to an array that doesn't contain it", () => { + const variableCategory: IVariableCategory = sampleWithRequiredData; + const variableCategoryCollection: IVariableCategory[] = [sampleWithPartialData]; + expectedResult = service.addVariableCategoryToCollectionIfMissing(variableCategoryCollection, variableCategory); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variableCategory); + }); + + it('should add only unique VariableCategory to an array', () => { + const variableCategoryArray: IVariableCategory[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const variableCategoryCollection: IVariableCategory[] = [sampleWithRequiredData]; + expectedResult = service.addVariableCategoryToCollectionIfMissing(variableCategoryCollection, ...variableCategoryArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const variableCategory: IVariableCategory = sampleWithRequiredData; + const variableCategory2: IVariableCategory = sampleWithPartialData; + expectedResult = service.addVariableCategoryToCollectionIfMissing([], variableCategory, variableCategory2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variableCategory); + expect(expectedResult).toContain(variableCategory2); + }); + + it('should accept null and undefined values', () => { + const variableCategory: IVariableCategory = sampleWithRequiredData; + expectedResult = service.addVariableCategoryToCollectionIfMissing([], null, variableCategory, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variableCategory); + }); + + it('should return initial array if no VariableCategory is added', () => { + const variableCategoryCollection: IVariableCategory[] = [sampleWithRequiredData]; + expectedResult = service.addVariableCategoryToCollectionIfMissing(variableCategoryCollection, undefined, null); + expect(expectedResult).toEqual(variableCategoryCollection); + }); + }); + + describe('compareVariableCategory', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareVariableCategory(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareVariableCategory(entity1, entity2); + const compareResult2 = service.compareVariableCategory(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareVariableCategory(entity1, entity2); + const compareResult2 = service.compareVariableCategory(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareVariableCategory(entity1, entity2); + const compareResult2 = service.compareVariableCategory(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/variable-category/service/variable-category.service.ts b/src/main/webapp/app/entities/variable-category/service/variable-category.service.ts new file mode 100644 index 0000000..c823613 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/service/variable-category.service.ts @@ -0,0 +1,35 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IVariableCategory, NewVariableCategory } from '../variable-category.model'; + +export type PartialUpdateVariableCategory = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class VariableCategoryService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/variable-categories'); + getResourceUrl(): string { return this.resourceUrl; } + + queryByScope(scopeId: number): Observable { + return this.http.get(`${this.resourceUrl}/byScope/${scopeId}`, { observe: 'response' }); + } + + queryByScopeForData(scopeId: number): Observable { + return this.http.get(`${this.resourceUrl}/byScope/forData/${scopeId}`, { observe: 'response' }); + } + + queryByScopeForFactor(scopeId: number): Observable { + return this.http.get(`${this.resourceUrl}/byScope/forFactor/${scopeId}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/variable-category/update/variable-category-form.service.spec.ts b/src/main/webapp/app/entities/variable-category/update/variable-category-form.service.spec.ts new file mode 100644 index 0000000..3d36322 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/update/variable-category-form.service.spec.ts @@ -0,0 +1,94 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../variable-category.test-samples'; + +import { VariableCategoryFormService } from './variable-category-form.service'; + +describe('VariableCategory Form Service', () => { + let service: VariableCategoryFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VariableCategoryFormService); + }); + + describe('Service methods', () => { + describe('createVariableCategoryFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createVariableCategoryFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + version: expect.any(Object), + variableScope: expect.any(Object), + }), + ); + }); + + it('passing IVariableCategory should create a new form with FormGroup', () => { + const formGroup = service.createVariableCategoryFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + version: expect.any(Object), + variableScope: expect.any(Object), + }), + ); + }); + }); + + describe('getVariableCategory', () => { + it('should return NewVariableCategory for default VariableCategory initial value', () => { + const formGroup = service.createVariableCategoryFormGroup(sampleWithNewData); + + const variableCategory = service.getVariableCategory(formGroup) as any; + + expect(variableCategory).toMatchObject(sampleWithNewData); + }); + + it('should return NewVariableCategory for empty VariableCategory initial value', () => { + const formGroup = service.createVariableCategoryFormGroup(); + + const variableCategory = service.getVariableCategory(formGroup) as any; + + expect(variableCategory).toMatchObject({}); + }); + + it('should return IVariableCategory', () => { + const formGroup = service.createVariableCategoryFormGroup(sampleWithRequiredData); + + const variableCategory = service.getVariableCategory(formGroup) as any; + + expect(variableCategory).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IVariableCategory should not enable id FormControl', () => { + const formGroup = service.createVariableCategoryFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewVariableCategory should disable id FormControl', () => { + const formGroup = service.createVariableCategoryFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-category/update/variable-category-form.service.ts b/src/main/webapp/app/entities/variable-category/update/variable-category-form.service.ts new file mode 100644 index 0000000..19ef61e --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/update/variable-category-form.service.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IVariableCategory, NewVariableCategory } from '../variable-category.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IVariableCategory for edit and NewVariableCategoryFormGroupInput for create. + */ +type VariableCategoryFormGroupInput = IVariableCategory | PartialWithRequiredKeyOf; + +type VariableCategoryFormDefaults = Pick; + +type VariableCategoryFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + description: FormControl; + version: FormControl; + variableScope: FormControl; + hiddenForData: FormControl; + hiddenForFactor: FormControl; +}; + +export type VariableCategoryFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class VariableCategoryFormService { + createVariableCategoryFormGroup(variableCategory: VariableCategoryFormGroupInput = { id: null }): VariableCategoryFormGroup { + const variableCategoryRawValue = { + ...this.getFormDefaults(), + ...variableCategory, + }; + return new FormGroup({ + id: new FormControl( + { value: variableCategoryRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(variableCategoryRawValue.code, { + validators: [Validators.required, Validators.pattern('^[0-9]*$')], + }), + name: new FormControl(variableCategoryRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(variableCategoryRawValue.description, { + validators: [Validators.required], + }), + version: new FormControl(variableCategoryRawValue.version), + variableScope: new FormControl(variableCategoryRawValue.variableScope), + hiddenForData: new FormControl(variableCategoryRawValue.hiddenForData), + hiddenForFactor: new FormControl(variableCategoryRawValue.hiddenForFactor), + }); + } + + getVariableCategory(form: VariableCategoryFormGroup): IVariableCategory | NewVariableCategory { + return form.getRawValue() as IVariableCategory | NewVariableCategory; + } + + resetForm(form: VariableCategoryFormGroup, variableCategory: VariableCategoryFormGroupInput): void { + const variableCategoryRawValue = { ...this.getFormDefaults(), ...variableCategory }; + form.reset( + { + ...variableCategoryRawValue, + id: { value: variableCategoryRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): VariableCategoryFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.html b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.html new file mode 100644 index 0000000..98bfffc --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.html @@ -0,0 +1,165 @@ + +
+
+
+

+ Criar ou editar Variable Category +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + +
+
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.pattern) { + Este campo deve estar dentro do seguinte padrão Code. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+
+ + @if (editForm.get('hiddenForData')!.invalid && (editForm.get('hiddenForData')!.dirty || editForm.get('hiddenForData')!.touched)) { +
+ @if (editForm.get('hiddenForData')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ +
+
+ + @if (editForm.get('hiddenForFactor')!.invalid && (editForm.get('hiddenForFactor')!.dirty || editForm.get('hiddenForFactor')!.touched)) { +
+ @if (editForm.get('hiddenForFactor')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.html.old b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.html.old new file mode 100644 index 0000000..7cc222f --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.html.old @@ -0,0 +1,136 @@ +
+
+
+

+ Criar ou editar Variable Category +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.pattern) { + Este campo deve estar dentro do seguinte padrão Code. + } +
+ } +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.spec.ts b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.spec.ts new file mode 100644 index 0000000..409f6eb --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.spec.ts @@ -0,0 +1,164 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { VariableCategoryService } from '../service/variable-category.service'; +import { IVariableCategory } from '../variable-category.model'; +import { VariableCategoryFormService } from './variable-category-form.service'; + +import { VariableCategoryUpdateComponent } from './variable-category-update.component'; + +describe('VariableCategory Management Update Component', () => { + let comp: VariableCategoryUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let variableCategoryFormService: VariableCategoryFormService; + let variableCategoryService: VariableCategoryService; + let variableScopeService: VariableScopeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableCategoryUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(VariableCategoryUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableCategoryUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + variableCategoryFormService = TestBed.inject(VariableCategoryFormService); + variableCategoryService = TestBed.inject(VariableCategoryService); + variableScopeService = TestBed.inject(VariableScopeService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call VariableScope query and add missing value', () => { + const variableCategory: IVariableCategory = { id: 456 }; + const variableScope: IVariableScope = { id: 5410 }; + variableCategory.variableScope = variableScope; + + const variableScopeCollection: IVariableScope[] = [{ id: 6574 }]; + jest.spyOn(variableScopeService, 'query').mockReturnValue(of(new HttpResponse({ body: variableScopeCollection }))); + const additionalVariableScopes = [variableScope]; + const expectedCollection: IVariableScope[] = [...additionalVariableScopes, ...variableScopeCollection]; + jest.spyOn(variableScopeService, 'addVariableScopeToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ variableCategory }); + comp.ngOnInit(); + + expect(variableScopeService.query).toHaveBeenCalled(); + expect(variableScopeService.addVariableScopeToCollectionIfMissing).toHaveBeenCalledWith( + variableScopeCollection, + ...additionalVariableScopes.map(expect.objectContaining), + ); + expect(comp.variableScopesSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const variableCategory: IVariableCategory = { id: 456 }; + const variableScope: IVariableScope = { id: 9890 }; + variableCategory.variableScope = variableScope; + + activatedRoute.data = of({ variableCategory }); + comp.ngOnInit(); + + expect(comp.variableScopesSharedCollection).toContain(variableScope); + expect(comp.variableCategory).toEqual(variableCategory); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableCategory = { id: 123 }; + jest.spyOn(variableCategoryFormService, 'getVariableCategory').mockReturnValue(variableCategory); + jest.spyOn(variableCategoryService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableCategory }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variableCategory })); + saveSubject.complete(); + + // THEN + expect(variableCategoryFormService.getVariableCategory).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(variableCategoryService.update).toHaveBeenCalledWith(expect.objectContaining(variableCategory)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableCategory = { id: 123 }; + jest.spyOn(variableCategoryFormService, 'getVariableCategory').mockReturnValue({ id: null }); + jest.spyOn(variableCategoryService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableCategory: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variableCategory })); + saveSubject.complete(); + + // THEN + expect(variableCategoryFormService.getVariableCategory).toHaveBeenCalled(); + expect(variableCategoryService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableCategory = { id: 123 }; + jest.spyOn(variableCategoryService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableCategory }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(variableCategoryService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareVariableScope', () => { + it('Should forward to variableScopeService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(variableScopeService, 'compareVariableScope'); + comp.compareVariableScope(entity, entity2); + expect(variableScopeService.compareVariableScope).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.ts b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.ts new file mode 100644 index 0000000..413587c --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/update/variable-category-update.component.ts @@ -0,0 +1,107 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { IVariableCategory } from '../variable-category.model'; +import { VariableCategoryService } from '../service/variable-category.service'; +import { VariableCategoryFormService, VariableCategoryFormGroup } from './variable-category-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-variable-category-update', + templateUrl: './variable-category-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class VariableCategoryUpdateComponent implements OnInit { + isSaving = false; + variableCategory: IVariableCategory | null = null; + + variableScopesSharedCollection: IVariableScope[] = []; + + protected variableCategoryService = inject(VariableCategoryService); + protected variableCategoryFormService = inject(VariableCategoryFormService); + protected variableScopeService = inject(VariableScopeService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: VariableCategoryFormGroup = this.variableCategoryFormService.createVariableCategoryFormGroup(); + + compareVariableScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ variableCategory }) => { + this.variableCategory = variableCategory; + if (variableCategory) { + this.updateForm(variableCategory); + } + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const variableCategory = this.variableCategoryFormService.getVariableCategory(this.editForm); + if (variableCategory.id !== null) { + this.subscribeToSaveResponse(this.variableCategoryService.update(variableCategory)); + } else { + this.subscribeToSaveResponse(this.variableCategoryService.create(variableCategory)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(variableCategory: IVariableCategory): void { + this.variableCategory = variableCategory; + this.variableCategoryFormService.resetForm(this.editForm, variableCategory); + + this.variableScopesSharedCollection = this.variableScopeService.addEntityToCollectionIfMissing( + this.variableScopesSharedCollection, + variableCategory.variableScope, + ); + } + + protected loadRelationshipsOptions(): void { + this.variableScopeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((variableScopes: IVariableScope[]) => + this.variableScopeService.addEntityToCollectionIfMissing( + variableScopes, + this.variableCategory?.variableScope, + ), + ), + ) + .subscribe((variableScopes: IVariableScope[]) => (this.variableScopesSharedCollection = variableScopes)); + } +} diff --git a/src/main/webapp/app/entities/variable-category/variable-category.model.ts b/src/main/webapp/app/entities/variable-category/variable-category.model.ts new file mode 100644 index 0000000..10ba797 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/variable-category.model.ts @@ -0,0 +1,14 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; + +export interface IVariableCategory extends IResilientEntity { + code?: string | null; + name?: string | null; + description?: string | null; + variableScope?: Pick | null; + hiddenForData?: boolean | null; + hiddenForFactor?: boolean | null; +} + +export type NewVariableCategory = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/variable-category/variable-category.routes.ts b/src/main/webapp/app/entities/variable-category/variable-category.routes.ts new file mode 100644 index 0000000..545bc62 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/variable-category.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { VariableCategoryComponent } from './list/variable-category.component'; +import { VariableCategoryDetailComponent } from './detail/variable-category-detail.component'; +import { VariableCategoryUpdateComponent } from './update/variable-category-update.component'; +import VariableCategoryResolve from './route/variable-category-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { VariableCategoryService } from './service/variable-category.service'; +import { Resources } from 'app/security/resources.model'; + +const variableCategoryRoute: Routes = [ + { + path: '', + component: VariableCategoryComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'variableScope.code,' + ASC, + action: SecurityAction.READ, + service: VariableCategoryService, + resource: Resources.VARIABLE_CATEGORY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: VariableCategoryDetailComponent, + resolve: { + variableCategory: VariableCategoryResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: VariableCategoryService, + resource: Resources.VARIABLE_CATEGORY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: VariableCategoryUpdateComponent, + resolve: { + variableCategory: VariableCategoryResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: VariableCategoryService, + resource: Resources.VARIABLE_CATEGORY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: VariableCategoryUpdateComponent, + resolve: { + variableCategory: VariableCategoryResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: VariableCategoryService, + resource: Resources.VARIABLE_CATEGORY, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default variableCategoryRoute; diff --git a/src/main/webapp/app/entities/variable-category/variable-category.test-samples.ts b/src/main/webapp/app/entities/variable-category/variable-category.test-samples.ts new file mode 100644 index 0000000..9c82144 --- /dev/null +++ b/src/main/webapp/app/entities/variable-category/variable-category.test-samples.ts @@ -0,0 +1,35 @@ +import { IVariableCategory, NewVariableCategory } from './variable-category.model'; + +export const sampleWithRequiredData: IVariableCategory = { + id: 17541, + code: '7', + name: 'seemingly sidestream', + description: 'ancient wisely around', +}; + +export const sampleWithPartialData: IVariableCategory = { + id: 22356, + code: '867', + name: 'self-reliant molecule gosh', + description: 'at underrate wasting', +}; + +export const sampleWithFullData: IVariableCategory = { + id: 8825, + code: '7991', + name: 'similar', + description: 'ha outgoing', + version: 4416, +}; + +export const sampleWithNewData: NewVariableCategory = { + code: '16', + name: 'yum confer for', + description: 'gosh now brave', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.html b/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.html new file mode 100644 index 0000000..8487893 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (variableClassType) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.spec.ts new file mode 100644 index 0000000..16c0c26 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { VariableClassTypeService } from '../service/variable-class-type.service'; + +import { VariableClassTypeDeleteDialogComponent } from './variable-class-type-delete-dialog.component'; + +describe('VariableClassType Management Delete Component', () => { + let comp: VariableClassTypeDeleteDialogComponent; + let fixture: ComponentFixture; + let service: VariableClassTypeService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableClassTypeDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(VariableClassTypeDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(VariableClassTypeDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableClassTypeService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.ts b/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.ts new file mode 100644 index 0000000..9f78949 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/delete/variable-class-type-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IVariableClassType } from '../variable-class-type.model'; +import { VariableClassTypeService } from '../service/variable-class-type.service'; + +@Component({ + standalone: true, + templateUrl: './variable-class-type-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class VariableClassTypeDeleteDialogComponent { + variableClassType?: IVariableClassType; + + protected variableClassTypeService = inject(VariableClassTypeService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.variableClassTypeService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.html b/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.html new file mode 100644 index 0000000..554a0a7 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.html @@ -0,0 +1,80 @@ + +
+
+ @if (variableClassType()) { +
+

Variable Class Type

+ +
+ + + + + +
+
Name
+
+ {{ variableClassType()!.name }} +
+
Description
+
+ {{ variableClassType()!.description }} +
+
Label
+
+ {{ variableClassType()!.label }} +
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + +
CodeName
{{i+1}} + {{ variableClass.code }} + + {{ variableClass.name }} +
+
+
+ + + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.spec.ts b/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.spec.ts new file mode 100644 index 0000000..502b42d --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { VariableClassTypeDetailComponent } from './variable-class-type-detail.component'; + +describe('VariableClassType Management Detail Component', () => { + let comp: VariableClassTypeDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VariableClassTypeDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: VariableClassTypeDetailComponent, + resolve: { variableClassType: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(VariableClassTypeDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VariableClassTypeDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load variableClassType on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', VariableClassTypeDetailComponent); + + // THEN + expect(instance.variableClassType()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.ts b/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.ts new file mode 100644 index 0000000..4ffb578 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/detail/variable-class-type-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IVariableClassType } from '../variable-class-type.model'; + +@Component({ + standalone: true, + selector: 'jhi-variable-class-type-detail', + templateUrl: './variable-class-type-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class VariableClassTypeDetailComponent { + variableClassType = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.html b/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.html new file mode 100644 index 0000000..f2ba298 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.html @@ -0,0 +1,109 @@ + +
+

+ Variable Class Types + +
+ + + +
+

+ + + + + + @if (variableClassTypes?.length === 0) { +
+ Nenhum Variable Class Types encontrado +
+ } + + @if (variableClassTypes && variableClassTypes.length > 0) { +
+ + + + + + + + + + + + + + @for (variableClassType of variableClassTypes; track trackId) { + + + + + + + + + + } + +
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Label + + +
+
{{ variableClassType.name }}{{ variableClassType.description }}{{ variableClassType.label }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.spec.ts b/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.spec.ts new file mode 100644 index 0000000..d53eab7 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../variable-class-type.test-samples'; +import { VariableClassTypeService } from '../service/variable-class-type.service'; + +import { VariableClassTypeComponent } from './variable-class-type.component'; +import SpyInstance = jest.SpyInstance; + +describe('VariableClassType Management Component', () => { + let comp: VariableClassTypeComponent; + let fixture: ComponentFixture; + let service: VariableClassTypeService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableClassTypeComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(VariableClassTypeComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableClassTypeComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableClassTypeService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.variableClassTypes?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to variableClassTypeService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getVariableClassTypeIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getVariableClassTypeIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.ts b/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.ts new file mode 100644 index 0000000..063eef8 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/list/variable-class-type.component.ts @@ -0,0 +1,121 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IVariableClassType } from '../variable-class-type.model'; +import { EntityArrayResponseType, VariableClassTypeService } from '../service/variable-class-type.service'; +import { VariableClassTypeDeleteDialogComponent } from '../delete/variable-class-type-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-variable-class-type', + templateUrl: './variable-class-type.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class VariableClassTypeComponent implements OnInit { + subscription: Subscription | null = null; + variableClassTypes?: IVariableClassType[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected variableClassTypeService = inject(VariableClassTypeService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IVariableClassType): number => this.variableClassTypeService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.variableClassTypes || this.variableClassTypes.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(variableClassType: IVariableClassType): void { + const modalRef = this.modalService.open(VariableClassTypeDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.variableClassType = variableClassType; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.variableClassTypes = this.refineData(dataFromBody); + } + + protected refineData(data: IVariableClassType[]): IVariableClassType[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IVariableClassType[] | null): IVariableClassType[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.variableClassTypeService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/variable-class-type/route/variable-class-type-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/variable-class-type/route/variable-class-type-routing-resolve.service.spec.ts new file mode 100644 index 0000000..0bd9bf6 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/route/variable-class-type-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IVariableClassType } from '../variable-class-type.model'; +import { VariableClassTypeService } from '../service/variable-class-type.service'; + +import variableClassTypeResolve from './variable-class-type-routing-resolve.service'; + +describe('VariableClassType routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: VariableClassTypeService; + let resultVariableClassType: IVariableClassType | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(VariableClassTypeService); + resultVariableClassType = undefined; + }); + + describe('resolve', () => { + it('should return IVariableClassType returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableClassTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableClassType = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariableClassType).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + variableClassTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableClassType = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultVariableClassType).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableClassTypeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableClassType = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariableClassType).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class-type/route/variable-class-type-routing-resolve.service.ts b/src/main/webapp/app/entities/variable-class-type/route/variable-class-type-routing-resolve.service.ts new file mode 100644 index 0000000..82ec589 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/route/variable-class-type-routing-resolve.service.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IVariableClassType } from '../variable-class-type.model'; +import { VariableClassTypeService } from '../service/variable-class-type.service'; + +const variableClassTypeResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(VariableClassTypeService) + .find(id) + .pipe( + mergeMap((variableClassType: HttpResponse) => { + if (variableClassType.body) { + const ut = of(variableClassType.body); + return of(variableClassType.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default variableClassTypeResolve; diff --git a/src/main/webapp/app/entities/variable-class-type/service/variable-class-type.service.spec.ts b/src/main/webapp/app/entities/variable-class-type/service/variable-class-type.service.spec.ts new file mode 100644 index 0000000..9b4744d --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/service/variable-class-type.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IVariableClassType } from '../variable-class-type.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../variable-class-type.test-samples'; + +import { VariableClassTypeService } from './variable-class-type.service'; + +const requireRestSample: IVariableClassType = { + ...sampleWithRequiredData, +}; + +describe('VariableClassType Service', () => { + let service: VariableClassTypeService; + let httpMock: HttpTestingController; + let expectedResult: IVariableClassType | IVariableClassType[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(VariableClassTypeService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a VariableClassType', () => { + const variableClassType = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(variableClassType).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a VariableClassType', () => { + const variableClassType = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(variableClassType).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a VariableClassType', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of VariableClassType', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a VariableClassType', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addVariableClassTypeToCollectionIfMissing', () => { + it('should add a VariableClassType to an empty array', () => { + const variableClassType: IVariableClassType = sampleWithRequiredData; + expectedResult = service.addVariableClassTypeToCollectionIfMissing([], variableClassType); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variableClassType); + }); + + it('should not add a VariableClassType to an array that contains it', () => { + const variableClassType: IVariableClassType = sampleWithRequiredData; + const variableClassTypeCollection: IVariableClassType[] = [ + { + ...variableClassType, + }, + sampleWithPartialData, + ]; + expectedResult = service.addVariableClassTypeToCollectionIfMissing(variableClassTypeCollection, variableClassType); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a VariableClassType to an array that doesn't contain it", () => { + const variableClassType: IVariableClassType = sampleWithRequiredData; + const variableClassTypeCollection: IVariableClassType[] = [sampleWithPartialData]; + expectedResult = service.addVariableClassTypeToCollectionIfMissing(variableClassTypeCollection, variableClassType); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variableClassType); + }); + + it('should add only unique VariableClassType to an array', () => { + const variableClassTypeArray: IVariableClassType[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const variableClassTypeCollection: IVariableClassType[] = [sampleWithRequiredData]; + expectedResult = service.addVariableClassTypeToCollectionIfMissing(variableClassTypeCollection, ...variableClassTypeArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const variableClassType: IVariableClassType = sampleWithRequiredData; + const variableClassType2: IVariableClassType = sampleWithPartialData; + expectedResult = service.addVariableClassTypeToCollectionIfMissing([], variableClassType, variableClassType2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variableClassType); + expect(expectedResult).toContain(variableClassType2); + }); + + it('should accept null and undefined values', () => { + const variableClassType: IVariableClassType = sampleWithRequiredData; + expectedResult = service.addVariableClassTypeToCollectionIfMissing([], null, variableClassType, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variableClassType); + }); + + it('should return initial array if no VariableClassType is added', () => { + const variableClassTypeCollection: IVariableClassType[] = [sampleWithRequiredData]; + expectedResult = service.addVariableClassTypeToCollectionIfMissing(variableClassTypeCollection, undefined, null); + expect(expectedResult).toEqual(variableClassTypeCollection); + }); + }); + + describe('compareVariableClassType', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareVariableClassType(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareVariableClassType(entity1, entity2); + const compareResult2 = service.compareVariableClassType(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareVariableClassType(entity1, entity2); + const compareResult2 = service.compareVariableClassType(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareVariableClassType(entity1, entity2); + const compareResult2 = service.compareVariableClassType(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class-type/service/variable-class-type.service.ts b/src/main/webapp/app/entities/variable-class-type/service/variable-class-type.service.ts new file mode 100644 index 0000000..e3675cb --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/service/variable-class-type.service.ts @@ -0,0 +1,26 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IVariableClassType, NewVariableClassType } from '../variable-class-type.model'; +import { IVariableClass } from 'app/entities/variable-class/variable-class.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class VariableClassTypeService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/variable-class-types'); + getResourceUrl(): string { return this.resourceUrl; } + + queryForVariable(variableId: number): Observable> { + return this.http.get(`${this.resourceUrl}/classes/forVariable/${variableId}`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-form.service.spec.ts b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-form.service.spec.ts new file mode 100644 index 0000000..a59a6d2 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-form.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../variable-class-type.test-samples'; + +import { VariableClassTypeFormService } from './variable-class-type-form.service'; + +describe('VariableClassType Form Service', () => { + let service: VariableClassTypeFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VariableClassTypeFormService); + }); + + describe('Service methods', () => { + describe('createVariableClassTypeFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createVariableClassTypeFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + label: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + + it('passing IVariableClassType should create a new form with FormGroup', () => { + const formGroup = service.createVariableClassTypeFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + label: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + }); + + describe('getVariableClassType', () => { + it('should return NewVariableClassType for default VariableClassType initial value', () => { + const formGroup = service.createVariableClassTypeFormGroup(sampleWithNewData); + + const variableClassType = service.getVariableClassType(formGroup) as any; + + expect(variableClassType).toMatchObject(sampleWithNewData); + }); + + it('should return NewVariableClassType for empty VariableClassType initial value', () => { + const formGroup = service.createVariableClassTypeFormGroup(); + + const variableClassType = service.getVariableClassType(formGroup) as any; + + expect(variableClassType).toMatchObject({}); + }); + + it('should return IVariableClassType', () => { + const formGroup = service.createVariableClassTypeFormGroup(sampleWithRequiredData); + + const variableClassType = service.getVariableClassType(formGroup) as any; + + expect(variableClassType).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IVariableClassType should not enable id FormControl', () => { + const formGroup = service.createVariableClassTypeFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewVariableClassType should disable id FormControl', () => { + const formGroup = service.createVariableClassTypeFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-form.service.ts b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-form.service.ts new file mode 100644 index 0000000..567f95a --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-form.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import { IVariableClassType, NewVariableClassType } from '../variable-class-type.model'; +import { VariableClassFormGroup, VariableClassFormService } from '../../variable-class/update/variable-class-form.service'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IVariableClassType for edit and NewVariableClassTypeFormGroupInput for create. + */ +type VariableClassTypeFormGroupInput = IVariableClassType | PartialWithRequiredKeyOf; + +type VariableClassTypeFormDefaults = Pick; + +type VariableClassTypeFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + description: FormControl; + label: FormControl; + variableClasses: FormArray; + version: FormControl; +}; + +export type VariableClassTypeFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class VariableClassTypeFormService { + constructor(private fb: FormBuilder, + private variableClassFormService: VariableClassFormService ) {} + + createVariableClassTypeFormGroup(variableClassType: VariableClassTypeFormGroupInput = { id: null }): VariableClassTypeFormGroup { + const variableClassTypeRawValue = { + ...this.getFormDefaults(), + ...variableClassType, + }; + + const items = variableClassType?.variableClasses??[]; + + return new FormGroup({ + id: new FormControl( + { value: variableClassTypeRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(variableClassTypeRawValue.code, { + validators: [Validators.required], + }), + name: new FormControl(variableClassTypeRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(variableClassTypeRawValue.description, { + validators: [Validators.required], + }), + label: new FormControl(variableClassTypeRawValue.label, { + validators: [Validators.required], + }), + variableClasses: new FormArray(items.map(item => this.variableClassFormService.createVariableClassFormGroup(item))), + version: new FormControl(variableClassTypeRawValue.version), + }); + } + + getVariableClassType(form: VariableClassTypeFormGroup): IVariableClassType | NewVariableClassType { + return form.getRawValue() as IVariableClassType | NewVariableClassType; + } + + resetForm(form: VariableClassTypeFormGroup, variableClassType: VariableClassTypeFormGroupInput): void { + const variableClassTypeRawValue = { ...this.getFormDefaults(), ...variableClassType }; + form.reset( + { + ...variableClassTypeRawValue, + id: { value: variableClassTypeRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): VariableClassTypeFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.html b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.html new file mode 100644 index 0000000..0fcb282 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.html @@ -0,0 +1,149 @@ + +
+
+
+

+ Criar ou editar Variable Class Type +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('label')!.invalid && (editForm.get('label')!.dirty || editForm.get('label')!.touched)) { +
+ @if (editForm.get('label')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
CodeName
{{i+1}} + + + + + +
+
+
+ +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.spec.ts b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.spec.ts new file mode 100644 index 0000000..e9d6b17 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { VariableClassTypeService } from '../service/variable-class-type.service'; +import { IVariableClassType } from '../variable-class-type.model'; +import { VariableClassTypeFormService } from './variable-class-type-form.service'; + +import { VariableClassTypeUpdateComponent } from './variable-class-type-update.component'; + +describe('VariableClassType Management Update Component', () => { + let comp: VariableClassTypeUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let variableClassTypeFormService: VariableClassTypeFormService; + let variableClassTypeService: VariableClassTypeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableClassTypeUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(VariableClassTypeUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableClassTypeUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + variableClassTypeFormService = TestBed.inject(VariableClassTypeFormService); + variableClassTypeService = TestBed.inject(VariableClassTypeService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should update editForm', () => { + const variableClassType: IVariableClassType = { id: 456 }; + + activatedRoute.data = of({ variableClassType }); + comp.ngOnInit(); + + expect(comp.variableClassType).toEqual(variableClassType); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableClassType = { id: 123 }; + jest.spyOn(variableClassTypeFormService, 'getVariableClassType').mockReturnValue(variableClassType); + jest.spyOn(variableClassTypeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableClassType }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variableClassType })); + saveSubject.complete(); + + // THEN + expect(variableClassTypeFormService.getVariableClassType).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(variableClassTypeService.update).toHaveBeenCalledWith(expect.objectContaining(variableClassType)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableClassType = { id: 123 }; + jest.spyOn(variableClassTypeFormService, 'getVariableClassType').mockReturnValue({ id: null }); + jest.spyOn(variableClassTypeService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableClassType: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variableClassType })); + saveSubject.complete(); + + // THEN + expect(variableClassTypeFormService.getVariableClassType).toHaveBeenCalled(); + expect(variableClassTypeService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableClassType = { id: 123 }; + jest.spyOn(variableClassTypeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableClassType }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(variableClassTypeService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.ts b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.ts new file mode 100644 index 0000000..968d65e --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/update/variable-class-type-update.component.ts @@ -0,0 +1,104 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule, FormArray } from '@angular/forms'; + +import { IVariableClassType } from '../variable-class-type.model'; +import { VariableClassTypeService } from '../service/variable-class-type.service'; +import { VariableClassTypeFormService, VariableClassTypeFormGroup } from './variable-class-type-form.service'; +import { VariableClassFormService } from 'app/entities/variable-class/update/variable-class-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-variable-class-type-update', + templateUrl: './variable-class-type-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class VariableClassTypeUpdateComponent implements OnInit { + isSaving = false; + variableClassType: IVariableClassType | null = null; + currentTab: string = 'classes'; // default tab + + protected variableClassTypeService = inject(VariableClassTypeService); + protected variableClassTypeFormService = inject(VariableClassTypeFormService); + protected variableClassFormService = inject(VariableClassFormService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: VariableClassTypeFormGroup = this.variableClassTypeFormService.createVariableClassTypeFormGroup(); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ variableClassType }) => { + this.variableClassType = variableClassType; + if (variableClassType) { + this.updateForm(variableClassType); + } + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const variableClassType = this.variableClassTypeFormService.getVariableClassType(this.editForm); + if (variableClassType.id !== null) { + this.subscribeToSaveResponse(this.variableClassTypeService.update(variableClassType)); + } else { + this.subscribeToSaveResponse(this.variableClassTypeService.create(variableClassType)); + } + } + + setTab(tabId: string): void { + this.currentTab = tabId; + } + + /* Nested collection methods : VariableClasse's */ + get variableClasses(): FormArray { + return this.editForm.get('variableClasses') as FormArray; + } + + addVariableClass(): void { + this.variableClasses.push( + this.variableClassFormService.createVariableClassFormGroup() + ); + } + + removeVariableClass(index: number): void { + this.variableClasses.removeAt(index); + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(variableClassType: IVariableClassType): void { + this.variableClassType = variableClassType; + this.variableClassTypeFormService.resetForm(this.editForm, variableClassType); + + // variable.variableClasses FormArray + variableClassType.variableClasses?.forEach(varClass => { + this.variableClasses.push(this.variableClassFormService.createVariableClassFormGroup(varClass)); + }); + } +} diff --git a/src/main/webapp/app/entities/variable-class-type/variable-class-type.model.ts b/src/main/webapp/app/entities/variable-class-type/variable-class-type.model.ts new file mode 100644 index 0000000..cf8aaac --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/variable-class-type.model.ts @@ -0,0 +1,13 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariableClass } from 'app/entities/variable-class/variable-class.model'; + +export interface IVariableClassType extends IResilientEntity { + code?: string | null; + name?: string | null; + description?: string | null; + label?: string | null; + variableClasses?: IVariableClass[] | null; +} + +export type NewVariableClassType = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/variable-class-type/variable-class-type.routes.ts b/src/main/webapp/app/entities/variable-class-type/variable-class-type.routes.ts new file mode 100644 index 0000000..557efe5 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/variable-class-type.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { VariableClassTypeComponent } from './list/variable-class-type.component'; +import { VariableClassTypeDetailComponent } from './detail/variable-class-type-detail.component'; +import { VariableClassTypeUpdateComponent } from './update/variable-class-type-update.component'; +import VariableClassTypeResolve from './route/variable-class-type-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { VariableClassTypeService } from './service/variable-class-type.service'; +import { Resources } from 'app/security/resources.model'; + +const variableClassTypeRoute: Routes = [ + { + path: '', + component: VariableClassTypeComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: VariableClassTypeService, + resource: Resources.VARIABLE_CLASS_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: VariableClassTypeDetailComponent, + resolve: { + variableClassType: VariableClassTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: VariableClassTypeService, + resource: Resources.VARIABLE_CLASS_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: VariableClassTypeUpdateComponent, + resolve: { + variableClassType: VariableClassTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: VariableClassTypeService, + resource: Resources.VARIABLE_CLASS_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: VariableClassTypeUpdateComponent, + resolve: { + variableClassType: VariableClassTypeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: VariableClassTypeService, + resource: Resources.VARIABLE_CLASS_TYPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default variableClassTypeRoute; diff --git a/src/main/webapp/app/entities/variable-class-type/variable-class-type.test-samples.ts b/src/main/webapp/app/entities/variable-class-type/variable-class-type.test-samples.ts new file mode 100644 index 0000000..bc0ebeb --- /dev/null +++ b/src/main/webapp/app/entities/variable-class-type/variable-class-type.test-samples.ts @@ -0,0 +1,35 @@ +import { IVariableClassType, NewVariableClassType } from './variable-class-type.model'; + +export const sampleWithRequiredData: IVariableClassType = { + id: 19315, + name: 'except barring upset', + description: 'hearty ew um', + label: 'label barring upset', +}; + +export const sampleWithPartialData: IVariableClassType = { + id: 22736, + name: 'except hm', + description: 'upon push deserted', + label: 'label barring upset', +}; + +export const sampleWithFullData: IVariableClassType = { + id: 579, + name: 'tatami', + description: 'barring really', + valueType: 'DECIMAL', + label: 'label barring upset', +}; + +export const sampleWithNewData: NewVariableClassType = { + name: 'reckless outgun', + description: 'though', + label: 'label barring upset', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/variable-class/service/variable-class.service.ts_DELETED b/src/main/webapp/app/entities/variable-class/service/variable-class.service.ts_DELETED new file mode 100644 index 0000000..b41b1c2 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class/service/variable-class.service.ts_DELETED @@ -0,0 +1,22 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IVariableClass, NewVariableClass } from '../variable-class.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class VariableClassService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/variable-classes'); + getResourceUrl(): string { return this.resourceUrl; } + +} diff --git a/src/main/webapp/app/entities/variable-class/update/variable-class-form.service.spec.ts b/src/main/webapp/app/entities/variable-class/update/variable-class-form.service.spec.ts new file mode 100644 index 0000000..0ea6225 --- /dev/null +++ b/src/main/webapp/app/entities/variable-class/update/variable-class-form.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../variable-class.test-samples'; + +import { VariableClassFormService } from './variable-class-form.service'; + +describe('VariableClass Form Service', () => { + let service: VariableClassFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VariableClassFormService); + }); + + describe('Service methods', () => { + describe('createVariableClassFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createVariableClassFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + }), + ); + }); + + it('passing IVariableClass should create a new form with FormGroup', () => { + const formGroup = service.createVariableClassFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + }), + ); + }); + }); + + describe('getVariableClass', () => { + it('should return NewVariableClass for default VariableClass initial value', () => { + const formGroup = service.createVariableClassFormGroup(sampleWithNewData); + + const variableClass = service.getVariableClass(formGroup) as any; + + expect(variableClass).toMatchObject(sampleWithNewData); + }); + + it('should return NewVariableClass for empty VariableClass initial value', () => { + const formGroup = service.createVariableClassFormGroup(); + + const variableClass = service.getVariableClass(formGroup) as any; + + expect(variableClass).toMatchObject({}); + }); + + it('should return IVariableClass', () => { + const formGroup = service.createVariableClassFormGroup(sampleWithRequiredData); + + const variableClass = service.getVariableClass(formGroup) as any; + + expect(variableClass).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IVariableClass should not enable id FormControl', () => { + const formGroup = service.createVariableClassFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewVariableClass should disable id FormControl', () => { + const formGroup = service.createVariableClassFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-class/update/variable-class-form.service.ts b/src/main/webapp/app/entities/variable-class/update/variable-class-form.service.ts new file mode 100644 index 0000000..b8f72da --- /dev/null +++ b/src/main/webapp/app/entities/variable-class/update/variable-class-form.service.ts @@ -0,0 +1,74 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IVariableClass, NewVariableClass } from '../variable-class.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IVariableClass for edit and NewVariableClassFormGroupInput for create. + */ +type VariableClassFormGroupInput = IVariableClass | PartialWithRequiredKeyOf; + +type VariableClassFormDefaults = Pick; + +type VariableClassFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + version: FormControl; + variableClassType: FormControl; +}; + +export type VariableClassFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class VariableClassFormService { + createVariableClassFormGroup(variableClass: VariableClassFormGroupInput = { id: null }): VariableClassFormGroup { + const variableClassRawValue = { + ...this.getFormDefaults(), + ...variableClass, + }; + return new FormGroup({ + id: new FormControl( + { value: variableClassRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(variableClassRawValue.code, { + validators: [Validators.required], + }), + name: new FormControl(variableClassRawValue.name, { + validators: [Validators.required], + }), + version: new FormControl(variableClassRawValue.version), + variableClassType: new FormControl(variableClassRawValue.variableClassType), + }); + } + + getVariableClass(form: VariableClassFormGroup): IVariableClass | NewVariableClass { + return form.getRawValue() as IVariableClass | NewVariableClass; + } + + resetForm(form: VariableClassFormGroup, variableClass: VariableClassFormGroupInput): void { + const variableClassRawValue = { ...this.getFormDefaults(), ...variableClass }; + form.reset( + { + ...variableClassRawValue, + id: { value: variableClassRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): VariableClassFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/variable-class/variable-class.model.ts b/src/main/webapp/app/entities/variable-class/variable-class.model.ts new file mode 100644 index 0000000..354b9fc --- /dev/null +++ b/src/main/webapp/app/entities/variable-class/variable-class.model.ts @@ -0,0 +1,11 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariableClassType } from 'app/entities/variable-class-type/variable-class-type.model'; + +export interface IVariableClass extends IResilientEntity { + code?: string | null; + name?: string | null; + variableClassType?: Pick | null; +} + +export type NewVariableClass = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/variable-class/variable-class.test-samples.ts b/src/main/webapp/app/entities/variable-class/variable-class.test-samples.ts new file mode 100644 index 0000000..3ee738e --- /dev/null +++ b/src/main/webapp/app/entities/variable-class/variable-class.test-samples.ts @@ -0,0 +1,32 @@ +import { IVariableClass, NewVariableClass } from './variable-class.model'; + +export const sampleWithRequiredData: IVariableClass = { + id: 22612, + code: 'starry how absolute', + name: 'ha worth', +}; + +export const sampleWithPartialData: IVariableClass = { + id: 22594, + code: 'tricky bogus why', + name: 'month but', + version: 17759, +}; + +export const sampleWithFullData: IVariableClass = { + id: 12741, + code: 'imbue ptarmigan unlike', + name: 'evolve pleased', + version: 31465, +}; + +export const sampleWithNewData: NewVariableClass = { + code: 'worse', + name: 'ouch', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.html b/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.html new file mode 100644 index 0000000..f16aa58 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.html @@ -0,0 +1,28 @@ +@if (variableScope) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.spec.ts new file mode 100644 index 0000000..f8c28a5 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { VariableScopeService } from '../service/variable-scope.service'; + +import { VariableScopeDeleteDialogComponent } from './variable-scope-delete-dialog.component'; + +describe('VariableScope Management Delete Component', () => { + let comp: VariableScopeDeleteDialogComponent; + let fixture: ComponentFixture; + let service: VariableScopeService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableScopeDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(VariableScopeDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(VariableScopeDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableScopeService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.ts b/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.ts new file mode 100644 index 0000000..9807458 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/delete/variable-scope-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IVariableScope } from '../variable-scope.model'; +import { VariableScopeService } from '../service/variable-scope.service'; + +@Component({ + standalone: true, + templateUrl: './variable-scope-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class VariableScopeDeleteDialogComponent { + variableScope?: IVariableScope; + + protected variableScopeService = inject(VariableScopeService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.variableScopeService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.html b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.html new file mode 100644 index 0000000..c02cfb8 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.html @@ -0,0 +1,69 @@ + +
+
+ @if (variableScope()) { +
+

Variable Scope

+ +
+ + + + + +
+
+ Code +
+
+ {{ variableScope()!.code }} +
+
+ Name +
+
+ {{ variableScope()!.name }} +
+
+ Description +
+
+ {{ variableScope()!.description }} +
+ + + +
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.html.old b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.html.old new file mode 100644 index 0000000..ec0584e --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.html.old @@ -0,0 +1,64 @@ +
+
+ @if (variableScope()) { +
+

Variable Scope

+ +
+ + + + + +
+
Código
+
+ {{ variableScope()!.id }} +
+
+ Code +
+
+ {{ variableScope()!.code }} +
+
+ Name +
+
+ {{ variableScope()!.name }} +
+
+ Description +
+
+ {{ variableScope()!.description }} +
+
+ Version +
+
+ {{ variableScope()!.version }} +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.spec.ts b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.spec.ts new file mode 100644 index 0000000..31996dd --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { VariableScopeDetailComponent } from './variable-scope-detail.component'; + +describe('VariableScope Management Detail Component', () => { + let comp: VariableScopeDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VariableScopeDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: VariableScopeDetailComponent, + resolve: { variableScope: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(VariableScopeDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VariableScopeDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load variableScope on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', VariableScopeDetailComponent); + + // THEN + expect(instance.variableScope()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.ts b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.ts new file mode 100644 index 0000000..e73624c --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/detail/variable-scope-detail.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IVariableScope } from '../variable-scope.model'; + +@Component({ + standalone: true, + selector: 'jhi-variable-scope-detail', + templateUrl: './variable-scope-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class VariableScopeDetailComponent { + variableScope = input(null); + + previousState(): void { + window.history.back(); + } +} diff --git a/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.html b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.html new file mode 100644 index 0000000..35e99fb --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.html @@ -0,0 +1,119 @@ + +
+

+ Variable Scopes + +
+ + + +
+

+ + + + + + @if (variableScopes?.length === 0) { +
+ Nenhum Variable Scopes encontrado +
+ } + + @if (variableScopes && variableScopes.length > 0) { +
+ + + + + + + + + + + + + + @for (variableScope of variableScopes; track trackId) { + + + + + + + + + + } + +
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
{{ variableScope.code }}{{ variableScope.name }}{{ variableScope.description }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.html.old b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.html.old new file mode 100644 index 0000000..cafaf93 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.html.old @@ -0,0 +1,110 @@ +
+

+ Variable Scopes + +
+ + + +
+

+ + + + + + @if (variableScopes?.length === 0) { +
+ Nenhum Variable Scopes encontrado +
+ } + + @if (variableScopes && variableScopes.length > 0) { +
+ + + + + + + + + + + + + @for (variableScope of variableScopes; track trackId) { + + + + + + + + + } + +
+
+ Código + + +
+
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Version + + +
+
+ {{ variableScope.id }} + {{ variableScope.code }}{{ variableScope.name }}{{ variableScope.description }}{{ variableScope.version }} +
+ + + Visualizar + + + + + Editar + + + +
+
+
+ } +
diff --git a/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.spec.ts b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.spec.ts new file mode 100644 index 0000000..7718a66 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.spec.ts @@ -0,0 +1,169 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../variable-scope.test-samples'; +import { VariableScopeService } from '../service/variable-scope.service'; + +import { VariableScopeComponent } from './variable-scope.component'; +import SpyInstance = jest.SpyInstance; + +describe('VariableScope Management Component', () => { + let comp: VariableScopeComponent; + let fixture: ComponentFixture; + let service: VariableScopeService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableScopeComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + }), + }, + }, + }, + ], + }) + .overrideTemplate(VariableScopeComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableScopeComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableScopeService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.variableScopes?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to variableScopeService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getVariableScopeIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getVariableScopeIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.ts b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.ts new file mode 100644 index 0000000..b97dac6 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/list/variable-scope.component.ts @@ -0,0 +1,121 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IVariableScope } from '../variable-scope.model'; +import { EntityArrayResponseType, VariableScopeService } from '../service/variable-scope.service'; +import { VariableScopeDeleteDialogComponent } from '../delete/variable-scope-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-variable-scope', + templateUrl: './variable-scope.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class VariableScopeComponent implements OnInit { + subscription: Subscription | null = null; + variableScopes?: IVariableScope[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected variableScopeService = inject(VariableScopeService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: IVariableScope): number => this.variableScopeService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.variableScopes || this.variableScopes.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(variableScope: IVariableScope): void { + const modalRef = this.modalService.open(VariableScopeDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.variableScope = variableScope; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.variableScopes = this.refineData(dataFromBody); + } + + protected refineData(data: IVariableScope[]): IVariableScope[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IVariableScope[] | null): IVariableScope[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.variableScopeService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/entities/variable-scope/route/variable-scope-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/variable-scope/route/variable-scope-routing-resolve.service.spec.ts new file mode 100644 index 0000000..6fa5ac2 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/route/variable-scope-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IVariableScope } from '../variable-scope.model'; +import { VariableScopeService } from '../service/variable-scope.service'; + +import variableScopeResolve from './variable-scope-routing-resolve.service'; + +describe('VariableScope routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: VariableScopeService; + let resultVariableScope: IVariableScope | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(VariableScopeService); + resultVariableScope = undefined; + }); + + describe('resolve', () => { + it('should return IVariableScope returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableScopeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableScope = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariableScope).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + variableScopeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableScope = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultVariableScope).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableScopeResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariableScope = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariableScope).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-scope/route/variable-scope-routing-resolve.service.ts b/src/main/webapp/app/entities/variable-scope/route/variable-scope-routing-resolve.service.ts new file mode 100644 index 0000000..1498568 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/route/variable-scope-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IVariableScope } from '../variable-scope.model'; +import { VariableScopeService } from '../service/variable-scope.service'; + +const variableScopeResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(VariableScopeService) + .find(id) + .pipe( + mergeMap((variableScope: HttpResponse) => { + if (variableScope.body) { + return of(variableScope.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default variableScopeResolve; diff --git a/src/main/webapp/app/entities/variable-scope/service/variable-scope.service.spec.ts b/src/main/webapp/app/entities/variable-scope/service/variable-scope.service.spec.ts new file mode 100644 index 0000000..ac0a0cc --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/service/variable-scope.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IVariableScope } from '../variable-scope.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../variable-scope.test-samples'; + +import { VariableScopeService } from './variable-scope.service'; + +const requireRestSample: IVariableScope = { + ...sampleWithRequiredData, +}; + +describe('VariableScope Service', () => { + let service: VariableScopeService; + let httpMock: HttpTestingController; + let expectedResult: IVariableScope | IVariableScope[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(VariableScopeService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a VariableScope', () => { + const variableScope = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(variableScope).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a VariableScope', () => { + const variableScope = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(variableScope).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a VariableScope', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of VariableScope', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a VariableScope', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addVariableScopeToCollectionIfMissing', () => { + it('should add a VariableScope to an empty array', () => { + const variableScope: IVariableScope = sampleWithRequiredData; + expectedResult = service.addVariableScopeToCollectionIfMissing([], variableScope); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variableScope); + }); + + it('should not add a VariableScope to an array that contains it', () => { + const variableScope: IVariableScope = sampleWithRequiredData; + const variableScopeCollection: IVariableScope[] = [ + { + ...variableScope, + }, + sampleWithPartialData, + ]; + expectedResult = service.addVariableScopeToCollectionIfMissing(variableScopeCollection, variableScope); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a VariableScope to an array that doesn't contain it", () => { + const variableScope: IVariableScope = sampleWithRequiredData; + const variableScopeCollection: IVariableScope[] = [sampleWithPartialData]; + expectedResult = service.addVariableScopeToCollectionIfMissing(variableScopeCollection, variableScope); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variableScope); + }); + + it('should add only unique VariableScope to an array', () => { + const variableScopeArray: IVariableScope[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const variableScopeCollection: IVariableScope[] = [sampleWithRequiredData]; + expectedResult = service.addVariableScopeToCollectionIfMissing(variableScopeCollection, ...variableScopeArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const variableScope: IVariableScope = sampleWithRequiredData; + const variableScope2: IVariableScope = sampleWithPartialData; + expectedResult = service.addVariableScopeToCollectionIfMissing([], variableScope, variableScope2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variableScope); + expect(expectedResult).toContain(variableScope2); + }); + + it('should accept null and undefined values', () => { + const variableScope: IVariableScope = sampleWithRequiredData; + expectedResult = service.addVariableScopeToCollectionIfMissing([], null, variableScope, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variableScope); + }); + + it('should return initial array if no VariableScope is added', () => { + const variableScopeCollection: IVariableScope[] = [sampleWithRequiredData]; + expectedResult = service.addVariableScopeToCollectionIfMissing(variableScopeCollection, undefined, null); + expect(expectedResult).toEqual(variableScopeCollection); + }); + }); + + describe('compareVariableScope', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareVariableScope(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareVariableScope(entity1, entity2); + const compareResult2 = service.compareVariableScope(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareVariableScope(entity1, entity2); + const compareResult2 = service.compareVariableScope(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareVariableScope(entity1, entity2); + const compareResult2 = service.compareVariableScope(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/variable-scope/service/variable-scope.service.ts b/src/main/webapp/app/entities/variable-scope/service/variable-scope.service.ts new file mode 100644 index 0000000..960eadf --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/service/variable-scope.service.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IVariableScope, NewVariableScope } from '../variable-scope.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class VariableScopeService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/variable-scopes'); + getResourceUrl(): string { return this.resourceUrl; } + + queryForData(): Observable { + return this.http.get(`${this.resourceUrl}/forData`, { observe: 'response' }); + } + + queryForFactor(): Observable { + return this.http.get(`${this.resourceUrl}/forFactor`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/entities/variable-scope/update/variable-scope-form.service.spec.ts b/src/main/webapp/app/entities/variable-scope/update/variable-scope-form.service.spec.ts new file mode 100644 index 0000000..55f6cf5 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/update/variable-scope-form.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../variable-scope.test-samples'; + +import { VariableScopeFormService } from './variable-scope-form.service'; + +describe('VariableScope Form Service', () => { + let service: VariableScopeFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VariableScopeFormService); + }); + + describe('Service methods', () => { + describe('createVariableScopeFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createVariableScopeFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + + it('passing IVariableScope should create a new form with FormGroup', () => { + const formGroup = service.createVariableScopeFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + version: expect.any(Object), + }), + ); + }); + }); + + describe('getVariableScope', () => { + it('should return NewVariableScope for default VariableScope initial value', () => { + const formGroup = service.createVariableScopeFormGroup(sampleWithNewData); + + const variableScope = service.getVariableScope(formGroup) as any; + + expect(variableScope).toMatchObject(sampleWithNewData); + }); + + it('should return NewVariableScope for empty VariableScope initial value', () => { + const formGroup = service.createVariableScopeFormGroup(); + + const variableScope = service.getVariableScope(formGroup) as any; + + expect(variableScope).toMatchObject({}); + }); + + it('should return IVariableScope', () => { + const formGroup = service.createVariableScopeFormGroup(sampleWithRequiredData); + + const variableScope = service.getVariableScope(formGroup) as any; + + expect(variableScope).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IVariableScope should not enable id FormControl', () => { + const formGroup = service.createVariableScopeFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewVariableScope should disable id FormControl', () => { + const formGroup = service.createVariableScopeFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-scope/update/variable-scope-form.service.ts b/src/main/webapp/app/entities/variable-scope/update/variable-scope-form.service.ts new file mode 100644 index 0000000..8126f54 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/update/variable-scope-form.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { IVariableScope, NewVariableScope } from '../variable-scope.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IVariableScope for edit and NewVariableScopeFormGroupInput for create. + */ +type VariableScopeFormGroupInput = IVariableScope | PartialWithRequiredKeyOf; + +type VariableScopeFormDefaults = Pick; + +type VariableScopeFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + description: FormControl; + version: FormControl; + hiddenForData: FormControl; + hiddenForFactor: FormControl; +}; + +export type VariableScopeFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class VariableScopeFormService { + createVariableScopeFormGroup(variableScope: VariableScopeFormGroupInput = { id: null }): VariableScopeFormGroup { + const variableScopeRawValue = { + ...this.getFormDefaults(), + ...variableScope, + }; + return new FormGroup({ + id: new FormControl( + { value: variableScopeRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(variableScopeRawValue.code, { + validators: [Validators.required, Validators.pattern('^[0-9]*$')], + }), + name: new FormControl(variableScopeRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(variableScopeRawValue.description, { + validators: [Validators.required], + }), + version: new FormControl(variableScopeRawValue.version), + hiddenForData: new FormControl(variableScopeRawValue.hiddenForData), + hiddenForFactor: new FormControl(variableScopeRawValue.hiddenForFactor), + }); + } + + getVariableScope(form: VariableScopeFormGroup): IVariableScope | NewVariableScope { + return form.getRawValue() as IVariableScope | NewVariableScope; + } + + resetForm(form: VariableScopeFormGroup, variableScope: VariableScopeFormGroupInput): void { + const variableScopeRawValue = { ...this.getFormDefaults(), ...variableScope }; + form.reset( + { + ...variableScopeRawValue, + id: { value: variableScopeRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): VariableScopeFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.html b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.html new file mode 100644 index 0000000..a50d2b8 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.html @@ -0,0 +1,143 @@ + +
+
+
+

+ Criar ou editar Variable Scope +

+ +
+ +
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.pattern) { + Este campo deve estar dentro do seguinte padrão Code. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+
+ + @if (editForm.get('hiddenForData')!.invalid && (editForm.get('hiddenForData')!.dirty || editForm.get('hiddenForData')!.touched)) { +
+ @if (editForm.get('hiddenForData')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ +
+
+ + @if (editForm.get('hiddenForFactor')!.invalid && (editForm.get('hiddenForFactor')!.dirty || editForm.get('hiddenForFactor')!.touched)) { +
+ @if (editForm.get('hiddenForFactor')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.html.old b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.html.old new file mode 100644 index 0000000..52a2a1a --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.html.old @@ -0,0 +1,117 @@ +
+
+
+

+ Criar ou editar Variable Scope +

+ +
+ + + @if (editForm.controls.id.value !== null) { +
+ + +
+ } + +
+ + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.pattern) { + Este campo deve estar dentro do seguinte padrão Code. + } +
+ } +
+ +
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.spec.ts b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.spec.ts new file mode 100644 index 0000000..a118cf0 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { VariableScopeService } from '../service/variable-scope.service'; +import { IVariableScope } from '../variable-scope.model'; +import { VariableScopeFormService } from './variable-scope-form.service'; + +import { VariableScopeUpdateComponent } from './variable-scope-update.component'; + +describe('VariableScope Management Update Component', () => { + let comp: VariableScopeUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let variableScopeFormService: VariableScopeFormService; + let variableScopeService: VariableScopeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableScopeUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(VariableScopeUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableScopeUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + variableScopeFormService = TestBed.inject(VariableScopeFormService); + variableScopeService = TestBed.inject(VariableScopeService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should update editForm', () => { + const variableScope: IVariableScope = { id: 456 }; + + activatedRoute.data = of({ variableScope }); + comp.ngOnInit(); + + expect(comp.variableScope).toEqual(variableScope); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableScope = { id: 123 }; + jest.spyOn(variableScopeFormService, 'getVariableScope').mockReturnValue(variableScope); + jest.spyOn(variableScopeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableScope }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variableScope })); + saveSubject.complete(); + + // THEN + expect(variableScopeFormService.getVariableScope).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(variableScopeService.update).toHaveBeenCalledWith(expect.objectContaining(variableScope)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableScope = { id: 123 }; + jest.spyOn(variableScopeFormService, 'getVariableScope').mockReturnValue({ id: null }); + jest.spyOn(variableScopeService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableScope: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variableScope })); + saveSubject.complete(); + + // THEN + expect(variableScopeFormService.getVariableScope).toHaveBeenCalled(); + expect(variableScopeService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const variableScope = { id: 123 }; + jest.spyOn(variableScopeService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variableScope }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(variableScopeService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.ts b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.ts new file mode 100644 index 0000000..527e537 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/update/variable-scope-update.component.ts @@ -0,0 +1,77 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { IVariableScope } from '../variable-scope.model'; +import { VariableScopeService } from '../service/variable-scope.service'; +import { VariableScopeFormService, VariableScopeFormGroup } from './variable-scope-form.service'; + +@Component({ + standalone: true, + selector: 'jhi-variable-scope-update', + templateUrl: './variable-scope-update.component.html', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class VariableScopeUpdateComponent implements OnInit { + isSaving = false; + variableScope: IVariableScope | null = null; + + protected variableScopeService = inject(VariableScopeService); + protected variableScopeFormService = inject(VariableScopeFormService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: VariableScopeFormGroup = this.variableScopeFormService.createVariableScopeFormGroup(); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ variableScope }) => { + this.variableScope = variableScope; + if (variableScope) { + this.updateForm(variableScope); + } + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const variableScope = this.variableScopeFormService.getVariableScope(this.editForm); + if (variableScope.id !== null) { + this.subscribeToSaveResponse(this.variableScopeService.update(variableScope)); + } else { + this.subscribeToSaveResponse(this.variableScopeService.create(variableScope)); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(variableScope: IVariableScope): void { + this.variableScope = variableScope; + this.variableScopeFormService.resetForm(this.editForm, variableScope); + } +} diff --git a/src/main/webapp/app/entities/variable-scope/variable-scope.model.ts b/src/main/webapp/app/entities/variable-scope/variable-scope.model.ts new file mode 100644 index 0000000..7fea6b8 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/variable-scope.model.ts @@ -0,0 +1,11 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +export interface IVariableScope extends IResilientEntity { + code?: string | null; + name?: string | null; + description?: string | null; + hiddenForData?: boolean | null; + hiddenForFactor?: boolean | null; +} + +export type NewVariableScope = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/variable-scope/variable-scope.routes.ts b/src/main/webapp/app/entities/variable-scope/variable-scope.routes.ts new file mode 100644 index 0000000..8727701 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/variable-scope.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { VariableScopeComponent } from './list/variable-scope.component'; +import { VariableScopeDetailComponent } from './detail/variable-scope-detail.component'; +import { VariableScopeUpdateComponent } from './update/variable-scope-update.component'; +import VariableScopeResolve from './route/variable-scope-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { VariableScopeService } from './service/variable-scope.service'; +import { Resources } from 'app/security/resources.model'; + +const variableScopeRoute: Routes = [ + { + path: '', + component: VariableScopeComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: VariableScopeService, + resource: Resources.VARIABLE_SCOPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: VariableScopeDetailComponent, + resolve: { + variableScope: VariableScopeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: VariableScopeService, + resource: Resources.VARIABLE_SCOPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: VariableScopeUpdateComponent, + resolve: { + variableScope: VariableScopeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: VariableScopeService, + resource: Resources.VARIABLE_SCOPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: VariableScopeUpdateComponent, + resolve: { + variableScope: VariableScopeResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: VariableScopeService, + resource: Resources.VARIABLE_SCOPE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default variableScopeRoute; diff --git a/src/main/webapp/app/entities/variable-scope/variable-scope.test-samples.ts b/src/main/webapp/app/entities/variable-scope/variable-scope.test-samples.ts new file mode 100644 index 0000000..e2a5133 --- /dev/null +++ b/src/main/webapp/app/entities/variable-scope/variable-scope.test-samples.ts @@ -0,0 +1,35 @@ +import { IVariableScope, NewVariableScope } from './variable-scope.model'; + +export const sampleWithRequiredData: IVariableScope = { + id: 13105, + code: '02168', + name: 'nut how', + description: 'around', +}; + +export const sampleWithPartialData: IVariableScope = { + id: 31707, + code: '00299', + name: 'though', + description: 'front', +}; + +export const sampleWithFullData: IVariableScope = { + id: 10652, + code: undefined, + name: 'how', + description: 'drat', + version: 4616, +}; + +export const sampleWithNewData: NewVariableScope = { + code: '7', + name: 'decay round', + description: 'which kookily through', + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/variable-units/service/variable-units.service.ts_DELETED b/src/main/webapp/app/entities/variable-units/service/variable-units.service.ts_DELETED new file mode 100644 index 0000000..56755b5 --- /dev/null +++ b/src/main/webapp/app/entities/variable-units/service/variable-units.service.ts_DELETED @@ -0,0 +1,21 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IVariableUnits, NewVariableUnits } from '../variable-units.model'; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class VariableUnitsService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/variable-units'); + getResourceUrl(): string { return this.resourceUrl; } +} diff --git a/src/main/webapp/app/entities/variable-units/update/variable-units-form.service.spec.ts b/src/main/webapp/app/entities/variable-units/update/variable-units-form.service.spec.ts new file mode 100644 index 0000000..15fc636 --- /dev/null +++ b/src/main/webapp/app/entities/variable-units/update/variable-units-form.service.spec.ts @@ -0,0 +1,90 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../variable-units.test-samples'; + +import { VariableUnitsFormService } from './variable-units-form.service'; + +describe('VariableUnits Form Service', () => { + let service: VariableUnitsFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VariableUnitsFormService); + }); + + describe('Service methods', () => { + describe('createVariableUnitsFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createVariableUnitsFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + unit: expect.any(Object), + }), + ); + }); + + it('passing IVariableUnits should create a new form with FormGroup', () => { + const formGroup = service.createVariableUnitsFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + version: expect.any(Object), + variable: expect.any(Object), + unit: expect.any(Object), + }), + ); + }); + }); + + describe('getVariableUnits', () => { + it('should return NewVariableUnits for default VariableUnits initial value', () => { + const formGroup = service.createVariableUnitsFormGroup(sampleWithNewData); + + const variableUnits = service.getVariableUnits(formGroup) as any; + + expect(variableUnits).toMatchObject(sampleWithNewData); + }); + + it('should return NewVariableUnits for empty VariableUnits initial value', () => { + const formGroup = service.createVariableUnitsFormGroup(); + + const variableUnits = service.getVariableUnits(formGroup) as any; + + expect(variableUnits).toMatchObject({}); + }); + + it('should return IVariableUnits', () => { + const formGroup = service.createVariableUnitsFormGroup(sampleWithRequiredData); + + const variableUnits = service.getVariableUnits(formGroup) as any; + + expect(variableUnits).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IVariableUnits should not enable id FormControl', () => { + const formGroup = service.createVariableUnitsFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewVariableUnits should disable id FormControl', () => { + const formGroup = service.createVariableUnitsFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable-units/update/variable-units-form.service.ts b/src/main/webapp/app/entities/variable-units/update/variable-units-form.service.ts new file mode 100644 index 0000000..b119856 --- /dev/null +++ b/src/main/webapp/app/entities/variable-units/update/variable-units-form.service.ts @@ -0,0 +1,75 @@ +import { Injectable, inject } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormArray, FormBuilder } from '@angular/forms'; + +import { IVariableUnits, NewVariableUnits } from '../variable-units.model'; +import { IUnitConverter } from 'app/entities/unit-converter/unit-converter.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IVariableUnits for edit and NewVariableUnitsFormGroupInput for create. + */ +type VariableUnitsFormGroupInput = IVariableUnits | PartialWithRequiredKeyOf; + +type VariableUnitsFormDefaults = Pick; + +type VariableUnitsFormGroupContent = { + id: FormControl; + version: FormControl; + variable: FormControl; + unit: FormControl; + unitConverter: FormControl; + unitConverterOptions: FormControl; +}; + +export type VariableUnitsFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class VariableUnitsFormService { + protected fb = inject(FormBuilder); + + createVariableUnitsFormGroup(variableUnits: VariableUnitsFormGroupInput = { id: null }): VariableUnitsFormGroup { + const variableUnitsRawValue = { + ...this.getFormDefaults(), + ...variableUnits, + }; + return new FormGroup({ + id: new FormControl( + { value: variableUnitsRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + version: new FormControl(variableUnitsRawValue.version), + variable: new FormControl(variableUnitsRawValue.variable), + unit: new FormControl(variableUnitsRawValue.unit, {validators: [Validators.required],}), + unitConverter: new FormControl(variableUnitsRawValue.unitConverter), + unitConverterOptions: new FormControl([]), + }); + } + + getVariableUnits(form: VariableUnitsFormGroup): IVariableUnits | NewVariableUnits { + return form.getRawValue() as IVariableUnits | NewVariableUnits; + } + + resetForm(form: VariableUnitsFormGroup, variableUnits: VariableUnitsFormGroupInput): void { + const variableUnitsRawValue = { ...this.getFormDefaults(), ...variableUnits }; + form.reset( + { + ...variableUnitsRawValue, + id: { value: variableUnitsRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): VariableUnitsFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/entities/variable-units/variable-units.model.ts b/src/main/webapp/app/entities/variable-units/variable-units.model.ts new file mode 100644 index 0000000..f77751e --- /dev/null +++ b/src/main/webapp/app/entities/variable-units/variable-units.model.ts @@ -0,0 +1,13 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IVariable } from 'app/entities/variable/variable.model'; +import { IUnit } from 'app/entities/unit/unit.model'; +import { IUnitConverter } from 'app/entities/unit-converter/unit-converter.model'; + +export interface IVariableUnits extends IResilientEntity { + variable?: Pick | null; + unit?: Pick | null; + unitConverter?: Pick | null; +} + +export type NewVariableUnits = Omit & { id: null }; diff --git a/src/main/webapp/app/entities/variable-units/variable-units.test-samples.ts b/src/main/webapp/app/entities/variable-units/variable-units.test-samples.ts new file mode 100644 index 0000000..98636d0 --- /dev/null +++ b/src/main/webapp/app/entities/variable-units/variable-units.test-samples.ts @@ -0,0 +1,23 @@ +import { IVariableUnits, NewVariableUnits } from './variable-units.model'; + +export const sampleWithRequiredData: IVariableUnits = { + id: 3566, +}; + +export const sampleWithPartialData: IVariableUnits = { + id: 11856, +}; + +export const sampleWithFullData: IVariableUnits = { + id: 20864, + version: 18445, +}; + +export const sampleWithNewData: NewVariableUnits = { + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.html b/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.html new file mode 100644 index 0000000..99628f4 --- /dev/null +++ b/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (variable) { +
+ + + + + +
+} diff --git a/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.spec.ts b/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.spec.ts new file mode 100644 index 0000000..bb8e554 --- /dev/null +++ b/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { VariableService } from '../service/variable.service'; + +import { VariableDeleteDialogComponent } from './variable-delete-dialog.component'; + +describe('Variable Management Delete Component', () => { + let comp: VariableDeleteDialogComponent; + let fixture: ComponentFixture; + let service: VariableService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableDeleteDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(VariableDeleteDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(VariableDeleteDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.ts b/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.ts new file mode 100644 index 0000000..85b9db7 --- /dev/null +++ b/src/main/webapp/app/entities/variable/delete/variable-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IVariable } from '../variable.model'; +import { VariableService } from '../service/variable.service'; + +@Component({ + standalone: true, + templateUrl: './variable-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class VariableDeleteDialogComponent { + variable?: IVariable; + + protected variableService = inject(VariableService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.variableService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/entities/variable/detail/variable-detail.component.css b/src/main/webapp/app/entities/variable/detail/variable-detail.component.css new file mode 100644 index 0000000..4e50e93 --- /dev/null +++ b/src/main/webapp/app/entities/variable/detail/variable-detail.component.css @@ -0,0 +1,3 @@ +:host .tab-content { + margin-bottom: 20px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/variable/detail/variable-detail.component.html b/src/main/webapp/app/entities/variable/detail/variable-detail.component.html new file mode 100644 index 0000000..9fc66c2 --- /dev/null +++ b/src/main/webapp/app/entities/variable/detail/variable-detail.component.html @@ -0,0 +1,196 @@ + +
+
+ @if (variable()) { +
+

Variable

+ +
+ + + + + +
+
Variable Scope
+
+ @if (variable()!.variableScope) { + + } +
+
Variable Category
+
+ @if (variable()!.variableCategory) { + + } +
+
Code
+
+ {{ variable()!.code }} +
+
Name
+
+ {{ variable()!.name }} +
+
Base Unit
+
+ @if (variable()!.baseUnit) { + + } +
+
+ Description +
+
+ {{ variable()!.description }} +
+
+ Value Scale +
+
+ {{ variable()!.valueScale }} +
+
+ + + + + + +
+
+
+
Input
+
{{ variable()!.input }}
+ +
Hidden for main ?
+
{{ variable()!.hiddenForMain }}
+ +
Input Mode
+
{{ variable()!.inputMode }}
+
+
+ +
+
+
Output
+
{{ variable()!.output }}
+ +
Output Formula
+
{{ variable()!.outputFormula }}
+
+
+ +
+
+
Class Type
+
{{ variable()!.variableClassType?.name }}
+ +
Class Label
+
{{ variable()!.variableClassLabel }}
+ +
Mandatory ?
+
{{ variable()!.variableClassMandatory }}
+
+
+ +
+ + + + + + + + + + + + + + + +
UnitConverter
{{i+1}} + {{ variableUnit.unit?.name }} + + {{ variableUnit.unitConverter?.name }} +
+
+
+ + + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/variable/detail/variable-detail.component.html.old b/src/main/webapp/app/entities/variable/detail/variable-detail.component.html.old new file mode 100644 index 0000000..53d5b69 --- /dev/null +++ b/src/main/webapp/app/entities/variable/detail/variable-detail.component.html.old @@ -0,0 +1,118 @@ +
+
+ @if (variable()) { +
+

Variable

+ +
+ + + + + +
+
Código
+
+ {{ variable()!.id }} +
+
Code
+
+ {{ variable()!.code }} +
+
+ Variable Index +
+
+ {{ variable()!.variableIndex }} +
+
Name
+
+ {{ variable()!.name }} +
+
+ Description +
+
+ {{ variable()!.description }} +
+
+ Input +
+
+ {{ variable()!.input }} +
+
+ Input Mode +
+
+ {{ + { null: '', DATAFILE: 'DATAFILE', MANUAL: 'MANUAL', INTEGRATION: 'INTEGRATION', ANY: 'ANY' }[variable()!.inputMode ?? 'null'] + }} +
+
+ Output +
+
+ {{ variable()!.output }} +
+
+ Output Formula +
+
+ {{ variable()!.outputFormula }} +
+
+ Version +
+
+ {{ variable()!.version }} +
+
Base Unit
+
+ @if (variable()!.baseUnit) { + + } +
+
Variable Scope
+
+ @if (variable()!.variableScope) { + + } +
+
Variable Category
+
+ @if (variable()!.variableCategory) { + + } +
+
+ + + + +
+ } +
+
diff --git a/src/main/webapp/app/entities/variable/detail/variable-detail.component.spec.ts b/src/main/webapp/app/entities/variable/detail/variable-detail.component.spec.ts new file mode 100644 index 0000000..9c13562 --- /dev/null +++ b/src/main/webapp/app/entities/variable/detail/variable-detail.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { VariableDetailComponent } from './variable-detail.component'; + +describe('Variable Management Detail Component', () => { + let comp: VariableDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VariableDetailComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: VariableDetailComponent, + resolve: { variable: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(VariableDetailComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VariableDetailComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load variable on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', VariableDetailComponent); + + // THEN + expect(instance.variable()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable/detail/variable-detail.component.ts b/src/main/webapp/app/entities/variable/detail/variable-detail.component.ts new file mode 100644 index 0000000..b2eb2b0 --- /dev/null +++ b/src/main/webapp/app/entities/variable/detail/variable-detail.component.ts @@ -0,0 +1,27 @@ +import { Component, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IVariable } from '../variable.model'; + +@Component({ + standalone: true, + selector: 'jhi-variable-detail', + templateUrl: './variable-detail.component.html', + styleUrl: './variable-detail.component.css', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], + +}) +export class VariableDetailComponent { + variable = input(null); + currentTab: string = 'input'; // default tab + + previousState(): void { + window.history.back(); + } + + setTab(tabId: string): void { + this.currentTab = tabId; + } +} diff --git a/src/main/webapp/app/entities/variable/list/variable-search-form.service.spec.ts b/src/main/webapp/app/entities/variable/list/variable-search-form.service.spec.ts new file mode 100644 index 0000000..1a162d6 --- /dev/null +++ b/src/main/webapp/app/entities/variable/list/variable-search-form.service.spec.ts @@ -0,0 +1,108 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../variable.test-samples'; + +import { VariableFormService } from './variable-form.service'; + +describe('Variable Form Service', () => { + let service: VariableFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VariableFormService); + }); + + describe('Service methods', () => { + describe('createVariableFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createVariableFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + variableIndex: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + input: expect.any(Object), + inputMode: expect.any(Object), + output: expect.any(Object), + outputFormula: expect.any(Object), + version: expect.any(Object), + baseUnit: expect.any(Object), + variableScope: expect.any(Object), + variableCategory: expect.any(Object), + }), + ); + }); + + it('passing IVariable should create a new form with FormGroup', () => { + const formGroup = service.createVariableFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + variableIndex: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + input: expect.any(Object), + inputMode: expect.any(Object), + output: expect.any(Object), + outputFormula: expect.any(Object), + version: expect.any(Object), + baseUnit: expect.any(Object), + variableScope: expect.any(Object), + variableCategory: expect.any(Object), + }), + ); + }); + }); + + describe('getVariable', () => { + it('should return NewVariable for default Variable initial value', () => { + const formGroup = service.createVariableFormGroup(sampleWithNewData); + + const variable = service.getVariable(formGroup) as any; + + expect(variable).toMatchObject(sampleWithNewData); + }); + + it('should return NewVariable for empty Variable initial value', () => { + const formGroup = service.createVariableFormGroup(); + + const variable = service.getVariable(formGroup) as any; + + expect(variable).toMatchObject({}); + }); + + it('should return IVariable', () => { + const formGroup = service.createVariableFormGroup(sampleWithRequiredData); + + const variable = service.getVariable(formGroup) as any; + + expect(variable).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IVariable should not enable id FormControl', () => { + const formGroup = service.createVariableFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewVariable should disable id FormControl', () => { + const formGroup = service.createVariableFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable/list/variable-search-form.service.ts b/src/main/webapp/app/entities/variable/list/variable-search-form.service.ts new file mode 100644 index 0000000..6b2e8cd --- /dev/null +++ b/src/main/webapp/app/entities/variable/list/variable-search-form.service.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import { IVariable, SearchVariable } from '../variable.model'; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IVariable for edit and NewVariableFormGroupInput for create. + */ +type VariableSearchFormGroupInput = SearchVariable; + +type VariableSearchFormDefaults = Pick; + +type VariableSearchFormGroupContent = { + code: FormControl; + variableIndex: FormControl; + name: FormControl; + description: FormControl; + input: FormControl; + inputMode: FormControl; + output: FormControl; + outputFormula: FormControl; + baseUnit: FormControl; + variableScopeId: FormControl; + variableCategoryId: FormControl; + variableClassTypeId: FormControl; + variableClassMandatory?: FormControl; + variableClassLabel?: FormControl; +}; + +export type VariableSearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class VariableSearchFormService { + constructor(private fb: FormBuilder) {} + + createVariableSearchFormGroup(variable: VariableSearchFormGroupInput = { code: null }): VariableSearchFormGroup { + const variableSearchRawValue = { + ...this.getSearchFormDefaults(), + ...variable, + }; + + return new FormGroup({ + code: new FormControl(variableSearchRawValue.code), + variableIndex: new FormControl(variableSearchRawValue.variableIndex), + name: new FormControl(variableSearchRawValue.name), + description: new FormControl(variableSearchRawValue.description), + input: new FormControl(variableSearchRawValue.input), + inputMode: new FormControl(variableSearchRawValue.inputMode), + output: new FormControl(variableSearchRawValue.output), + outputFormula: new FormControl(variableSearchRawValue.outputFormula), + baseUnit: new FormControl(variableSearchRawValue.baseUnit), + variableScopeId: new FormControl(variableSearchRawValue.variableScopeId), + variableCategoryId: new FormControl(variableSearchRawValue.variableCategoryId), + variableClassTypeId: new FormControl(variableSearchRawValue.variableClassTypeId), + variableClassMandatory: new FormControl(variableSearchRawValue.variableClassMandatory), + variableClassLabel: new FormControl(variableSearchRawValue.variableClassLabel) + }); + } + + getVariable(form: VariableSearchFormGroup): SearchVariable { + return form.getRawValue() as SearchVariable ; + } + + resetForm(form: VariableSearchFormGroup, variable: VariableSearchFormGroupInput): void { + const variableSearchRawValue = { ...this.getSearchFormDefaults(), ...variable }; + form.reset( + { + ...variableSearchRawValue, + id: { value: variableSearchRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): VariableSearchFormDefaults { + return { + input: null, + output: null, + variableClassMandatory: null, + }; + } +} diff --git a/src/main/webapp/app/entities/variable/list/variable.component.css b/src/main/webapp/app/entities/variable/list/variable.component.css new file mode 100644 index 0000000..a715865 --- /dev/null +++ b/src/main/webapp/app/entities/variable/list/variable.component.css @@ -0,0 +1,12 @@ +.form-check-input.indeterminate::after { + background-color: #0d6efd; /* Bootstrap primary color */ + opacity: 0.5; +} + +tr.search { + vertical-align: middle; +} + +.form-check-tristate fa-icon { + cursor: pointer; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/variable/list/variable.component.html b/src/main/webapp/app/entities/variable/list/variable.component.html new file mode 100644 index 0000000..a47f7e8 --- /dev/null +++ b/src/main/webapp/app/entities/variable/list/variable.component.html @@ -0,0 +1,225 @@ + +
+

+ Variables + +
+ + + +
+
+ +
+

+ + + + + + +
+ {{ ' ' // Wrap the all table in a form. This is the only way reactiveforms will work }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + @for (variable of variables; track trackId) { + + + + + + + + + + + } + +
+
+ Code + + +
+
+
+ Name + + +
+
+
+ Description + + +
+
+
+ Variable Scope + +
+
+
+ Variable Category + +
+
+
+ Input + + +
+
+
+ Output + + +
+
{{ variable.code }}{{ variable.name }}{{ variable.description }} + @if (variable.variableScope) { + + } + + @if (variable.variableCategory) { + + } + + + + + + + +
+ + + Visualizar + + + + + Editar + + + +
+
+
+
+ + @if (variables == null) { +
+ Seleccionar opções de filtro e executar pesquisa. +
+ } + + @if (variables?.length === 0) { +
+ Nenhum Variables encontrado +
+ } +
diff --git a/src/main/webapp/app/entities/variable/list/variable.component.spec.ts b/src/main/webapp/app/entities/variable/list/variable.component.spec.ts new file mode 100644 index 0000000..86279db --- /dev/null +++ b/src/main/webapp/app/entities/variable/list/variable.component.spec.ts @@ -0,0 +1,171 @@ +import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { sampleWithRequiredData } from '../variable.test-samples'; +import { VariableService } from '../service/variable.service'; + +import { VariableComponent } from './variable.component'; +import SpyInstance = jest.SpyInstance; + +describe('Variable Management Component', () => { + let comp: VariableComponent; + let fixture: ComponentFixture; + let service: VariableService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableComponent], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + 'filter[someId.in]': 'dc4279ea-cfb9-11ec-9d64-0242ac120002', + }), + ), + snapshot: { + queryParams: {}, + queryParamMap: jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + 'filter[someId.in]': 'dc4279ea-cfb9-11ec-9d64-0242ac120002', + }), + }, + }, + }, + ], + }) + .overrideTemplate(VariableComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableComponent); + comp = fixture.componentInstance; + service = TestBed.inject(VariableService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + jest + .spyOn(service, 'query') + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 123 }], + headers: new HttpHeaders({ + link: '; rel="next"', + }), + }), + ), + ) + .mockReturnValueOnce( + of( + new HttpResponse({ + body: [{ id: 456 }], + headers: new HttpHeaders({ + link: '; rel="prev",; rel="next"', + }), + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.variables?.[0]).toEqual(expect.objectContaining({ id: 123 })); + }); + + describe('trackId', () => { + it('Should forward to variableService', () => { + const entity = { id: 123 }; + jest.spyOn(service, 'getVariableIdentifier'); + const id = comp.trackId(0, entity); + expect(service.getVariableIdentifier).toHaveBeenCalledWith(entity); + expect(id).toBe(entity.id); + }); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // WHEN + comp.navigateToWithComponentValues({ predicate: 'non-existing-column', order: 'asc' }); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['non-existing-column,asc'], + }), + }), + ); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + describe('delete', () => { + let ngbModal: NgbModal; + let deleteModalMock: any; + + beforeEach(() => { + deleteModalMock = { componentInstance: {}, closed: new Subject() }; + // NgbModal is not a singleton using TestBed.inject. + // ngbModal = TestBed.inject(NgbModal); + ngbModal = (comp as any).modalService; + jest.spyOn(ngbModal, 'open').mockReturnValue(deleteModalMock); + }); + + it('on confirm should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next('deleted'); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).toHaveBeenCalled(); + }), + )); + + it('on dismiss should call load', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(comp, 'load'); + + // WHEN + comp.delete(sampleWithRequiredData); + deleteModalMock.closed.next(); + tick(); + + // THEN + expect(ngbModal.open).toHaveBeenCalled(); + expect(comp.load).not.toHaveBeenCalled(); + }), + )); + }); +}); diff --git a/src/main/webapp/app/entities/variable/list/variable.component.ts b/src/main/webapp/app/entities/variable/list/variable.component.ts new file mode 100644 index 0000000..df928fb --- /dev/null +++ b/src/main/webapp/app/entities/variable/list/variable.component.ts @@ -0,0 +1,256 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap, map } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpResponse } from '@angular/common/http'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { IVariable } from '../variable.model'; +import { EntityArrayResponseType, VariableService } from '../service/variable.service'; +import { VariableDeleteDialogComponent } from '../delete/variable-delete-dialog.component'; +import { VariableSearchFormService, VariableSearchFormGroup } from './variable-search-form.service'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; + +@Component({ + standalone: true, + selector: 'jhi-variable', + templateUrl: './variable.component.html', + styleUrl: './variable.component.css', + imports: [ + RouterModule, + FormsModule, + ReactiveFormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + MatProgressSpinnerModule, + ], +}) +export class VariableComponent implements OnInit { + subscription: Subscription | null = null; + variables?: IVariable[]; + isLoading = false; + isExporting: boolean = false; + + sortState = sortStateSignal({}); + + variableScopesSharedCollection: IVariableScope[] = []; + variableCategoriesSharedCollection: IVariableCategory[] = []; + + public router = inject(Router); + protected variableService = inject(VariableService); + protected variableSearchFormService = inject(VariableSearchFormService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + + // eslint-disable-next-line @typescript-eslint/member-ordering + searchForm: VariableSearchFormGroup = this.variableSearchFormService.createVariableSearchFormGroup(); + trackId = (_index: number, item: IVariable): number => this.variableService.getEntityIdentifier(item); + + compareVariableScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + compareVariableCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + ngOnInit(): void { + /* + DON'T LOAD on startup. Let the user select some filters + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => this.load()), + ) + .subscribe(); + */ + + // Listen to changes to the scope filter property + this.searchForm.get('variableScopeId')?.valueChanges.subscribe((scopeId) => { + this.loadCategoriesOfSelectedScope(scopeId); + this.searchForm.get('variableCategoryId')?.setValue(null); + }); + + this.loadRelationshipsOptions(); + } + + delete(variable: IVariable): void { + const modalRef = this.modalService.open(VariableDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.variable = variable; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + onSearch(): void { + this.load(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + // Tri-state checkbox helpers + isChecked(control: any): boolean { + if (control && control.value != null) { + return control.value; + } + return false; + } + + isUnChecked(control: any): boolean { + if (control && control.value != null) { + return !control.value; + } + return false; + } + + isUndetermined(control: any): boolean { + if (!control || control.value == null) { + return true; + } + return false; + } + + toggleState(control: any): void { + if (control) { + const value = control.value; + if (value === null) { + control.setValue(true); + } else if (value === true) { + control.setValue(false); + } else if (value === false) { + control.setValue(null); + } + } + } + + export(): void { + this.isExporting = true; + + this.variableService.export().subscribe((blob: Blob) => { + const a = document.createElement('a'); + const objectUrl = URL.createObjectURL(blob); + a.href = objectUrl; + a.download = 'report.xlsx'; + a.click(); + URL.revokeObjectURL(objectUrl); + + this.isExporting = false; + }); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.variables = this.refineData(dataFromBody); + } + + protected refineData(data: IVariable[]): IVariable[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IVariable[] | null): IVariable[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + + // Apply search values to the queryObject sent in the request, for server-side creation of a query criteria + const variableSearch = this.variableSearchFormService.getVariable(this.searchForm); + if (variableSearch) { + Object.entries(variableSearch).forEach(([key, value]) => { + var clause: string = "contains"; //Default + if (value !== null && value !== undefined) { + switch(typeof value) { + case 'string': { + // Don't search with an empty string criteria + if (value.trim() === "") return; + clause = "contains"; + break; + } + case 'boolean': { + clause = "equals"; + break; + } + case 'number': { + clause = "equals"; + break; + } + } + + queryObject[key + "." + clause] = value; + } + }); + } + + return this.variableService.queryByCriteria(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } + + protected loadCategoriesOfSelectedScope(scopeId: number|null|undefined): void { + if (scopeId) { + this.variableCategoryService + .queryByScope(scopeId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((cats: IVariableCategory[]) => (this.variableCategoriesSharedCollection = cats)); + } else { + this.variableCategoriesSharedCollection = []; + } + } + + protected loadRelationshipsOptions(): void { + this.variableScopeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((variableScopes: IVariableScope[]) => (this.variableScopesSharedCollection = variableScopes)); + + } +} diff --git a/src/main/webapp/app/entities/variable/route/variable-routing-resolve.service.spec.ts b/src/main/webapp/app/entities/variable/route/variable-routing-resolve.service.spec.ts new file mode 100644 index 0000000..f878b2f --- /dev/null +++ b/src/main/webapp/app/entities/variable/route/variable-routing-resolve.service.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { of } from 'rxjs'; + +import { IVariable } from '../variable.model'; +import { VariableService } from '../service/variable.service'; + +import variableResolve from './variable-routing-resolve.service'; + +describe('Variable routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: VariableService; + let resultVariable: IVariable | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(VariableService); + resultVariable = undefined; + }); + + describe('resolve', () => { + it('should return IVariable returned by find', () => { + // GIVEN + service.find = jest.fn(id => of(new HttpResponse({ body: { id } }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariable = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariable).toEqual({ id: 123 }); + }); + + it('should return null if id is not provided', () => { + // GIVEN + service.find = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + variableResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariable = result; + }, + }); + }); + + // THEN + expect(service.find).not.toBeCalled(); + expect(resultVariable).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'find').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { id: 123 }; + + // WHEN + TestBed.runInInjectionContext(() => { + variableResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultVariable = result; + }, + }); + }); + + // THEN + expect(service.find).toHaveBeenCalledWith(123); + expect(resultVariable).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable/route/variable-routing-resolve.service.ts b/src/main/webapp/app/entities/variable/route/variable-routing-resolve.service.ts new file mode 100644 index 0000000..2edb387 --- /dev/null +++ b/src/main/webapp/app/entities/variable/route/variable-routing-resolve.service.ts @@ -0,0 +1,29 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { IVariable } from '../variable.model'; +import { VariableService } from '../service/variable.service'; + +const variableResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(VariableService) + .find(id) + .pipe( + mergeMap((variable: HttpResponse) => { + if (variable.body) { + return of(variable.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default variableResolve; diff --git a/src/main/webapp/app/entities/variable/service/variable.service.spec.ts b/src/main/webapp/app/entities/variable/service/variable.service.spec.ts new file mode 100644 index 0000000..f5f97c1 --- /dev/null +++ b/src/main/webapp/app/entities/variable/service/variable.service.spec.ts @@ -0,0 +1,204 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { IVariable } from '../variable.model'; +import { sampleWithRequiredData, sampleWithNewData, sampleWithPartialData, sampleWithFullData } from '../variable.test-samples'; + +import { VariableService } from './variable.service'; + +const requireRestSample: IVariable = { + ...sampleWithRequiredData, +}; + +describe('Variable Service', () => { + let service: VariableService; + let httpMock: HttpTestingController; + let expectedResult: IVariable | IVariable[] | boolean | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + expectedResult = null; + service = TestBed.inject(VariableService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find an element', () => { + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.find(123).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should create a Variable', () => { + const variable = { ...sampleWithNewData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.create(variable).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should update a Variable', () => { + const variable = { ...sampleWithRequiredData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.update(variable).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PUT' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should partial update a Variable', () => { + const patchObject = { ...sampleWithPartialData }; + const returnedFromService = { ...requireRestSample }; + const expected = { ...sampleWithRequiredData }; + + service.partialUpdate(patchObject).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'PATCH' }); + req.flush(returnedFromService); + expect(expectedResult).toMatchObject(expected); + }); + + it('should return a list of Variable', () => { + const returnedFromService = { ...requireRestSample }; + + const expected = { ...sampleWithRequiredData }; + + service.query().subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush([returnedFromService]); + httpMock.verify(); + expect(expectedResult).toMatchObject([expected]); + }); + + it('should delete a Variable', () => { + const expected = true; + + service.delete(123).subscribe(resp => (expectedResult = resp.ok)); + + const req = httpMock.expectOne({ method: 'DELETE' }); + req.flush({ status: 200 }); + expect(expectedResult).toBe(expected); + }); + + describe('addVariableToCollectionIfMissing', () => { + it('should add a Variable to an empty array', () => { + const variable: IVariable = sampleWithRequiredData; + expectedResult = service.addVariableToCollectionIfMissing([], variable); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variable); + }); + + it('should not add a Variable to an array that contains it', () => { + const variable: IVariable = sampleWithRequiredData; + const variableCollection: IVariable[] = [ + { + ...variable, + }, + sampleWithPartialData, + ]; + expectedResult = service.addVariableToCollectionIfMissing(variableCollection, variable); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a Variable to an array that doesn't contain it", () => { + const variable: IVariable = sampleWithRequiredData; + const variableCollection: IVariable[] = [sampleWithPartialData]; + expectedResult = service.addVariableToCollectionIfMissing(variableCollection, variable); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variable); + }); + + it('should add only unique Variable to an array', () => { + const variableArray: IVariable[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const variableCollection: IVariable[] = [sampleWithRequiredData]; + expectedResult = service.addVariableToCollectionIfMissing(variableCollection, ...variableArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const variable: IVariable = sampleWithRequiredData; + const variable2: IVariable = sampleWithPartialData; + expectedResult = service.addVariableToCollectionIfMissing([], variable, variable2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(variable); + expect(expectedResult).toContain(variable2); + }); + + it('should accept null and undefined values', () => { + const variable: IVariable = sampleWithRequiredData; + expectedResult = service.addVariableToCollectionIfMissing([], null, variable, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(variable); + }); + + it('should return initial array if no Variable is added', () => { + const variableCollection: IVariable[] = [sampleWithRequiredData]; + expectedResult = service.addVariableToCollectionIfMissing(variableCollection, undefined, null); + expect(expectedResult).toEqual(variableCollection); + }); + }); + + describe('compareVariable', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareVariable(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { id: 123 }; + const entity2 = null; + + const compareResult1 = service.compareVariable(entity1, entity2); + const compareResult2 = service.compareVariable(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 456 }; + + const compareResult1 = service.compareVariable(entity1, entity2); + const compareResult2 = service.compareVariable(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { id: 123 }; + const entity2 = { id: 123 }; + + const compareResult1 = service.compareVariable(entity1, entity2); + const compareResult2 = service.compareVariable(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/main/webapp/app/entities/variable/service/variable.service.ts b/src/main/webapp/app/entities/variable/service/variable.service.ts new file mode 100644 index 0000000..bf9d66f --- /dev/null +++ b/src/main/webapp/app/entities/variable/service/variable.service.ts @@ -0,0 +1,37 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { IVariable, NewVariable } from '../variable.model'; +import { IUnit } from 'app/entities/unit/unit.model'; + + +export type PartialUpdateVariable = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class VariableService extends ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/variables'); + getResourceUrl(): string { return this.resourceUrl; } + + queryForBaseUnit(variableId: number): Observable> { + return this.http.get(`${this.resourceUrl}/${variableId}/baseUnit`, { observe: 'response' }); + } + + queryForManualInput(): Observable> { + return this.http.get(`${this.resourceUrl}/forManualInput`, { observe: 'response' }); + } + + export(): Observable { + return this.http.get(`${this.resourceUrl}/export`, { responseType: 'blob' }); + } +} diff --git a/src/main/webapp/app/entities/variable/update/variable-form.service.spec.ts b/src/main/webapp/app/entities/variable/update/variable-form.service.spec.ts new file mode 100644 index 0000000..1a162d6 --- /dev/null +++ b/src/main/webapp/app/entities/variable/update/variable-form.service.spec.ts @@ -0,0 +1,108 @@ +import { TestBed } from '@angular/core/testing'; + +import { sampleWithRequiredData, sampleWithNewData } from '../variable.test-samples'; + +import { VariableFormService } from './variable-form.service'; + +describe('Variable Form Service', () => { + let service: VariableFormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VariableFormService); + }); + + describe('Service methods', () => { + describe('createVariableFormGroup', () => { + it('should create a new form with FormControl', () => { + const formGroup = service.createVariableFormGroup(); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + variableIndex: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + input: expect.any(Object), + inputMode: expect.any(Object), + output: expect.any(Object), + outputFormula: expect.any(Object), + version: expect.any(Object), + baseUnit: expect.any(Object), + variableScope: expect.any(Object), + variableCategory: expect.any(Object), + }), + ); + }); + + it('passing IVariable should create a new form with FormGroup', () => { + const formGroup = service.createVariableFormGroup(sampleWithRequiredData); + + expect(formGroup.controls).toEqual( + expect.objectContaining({ + id: expect.any(Object), + code: expect.any(Object), + variableIndex: expect.any(Object), + name: expect.any(Object), + description: expect.any(Object), + input: expect.any(Object), + inputMode: expect.any(Object), + output: expect.any(Object), + outputFormula: expect.any(Object), + version: expect.any(Object), + baseUnit: expect.any(Object), + variableScope: expect.any(Object), + variableCategory: expect.any(Object), + }), + ); + }); + }); + + describe('getVariable', () => { + it('should return NewVariable for default Variable initial value', () => { + const formGroup = service.createVariableFormGroup(sampleWithNewData); + + const variable = service.getVariable(formGroup) as any; + + expect(variable).toMatchObject(sampleWithNewData); + }); + + it('should return NewVariable for empty Variable initial value', () => { + const formGroup = service.createVariableFormGroup(); + + const variable = service.getVariable(formGroup) as any; + + expect(variable).toMatchObject({}); + }); + + it('should return IVariable', () => { + const formGroup = service.createVariableFormGroup(sampleWithRequiredData); + + const variable = service.getVariable(formGroup) as any; + + expect(variable).toMatchObject(sampleWithRequiredData); + }); + }); + + describe('resetForm', () => { + it('passing IVariable should not enable id FormControl', () => { + const formGroup = service.createVariableFormGroup(); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, sampleWithRequiredData); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + + it('passing NewVariable should disable id FormControl', () => { + const formGroup = service.createVariableFormGroup(sampleWithRequiredData); + expect(formGroup.controls.id.disabled).toBe(true); + + service.resetForm(formGroup, { id: null }); + + expect(formGroup.controls.id.disabled).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable/update/variable-form.service.ts b/src/main/webapp/app/entities/variable/update/variable-form.service.ts new file mode 100644 index 0000000..59c1c7a --- /dev/null +++ b/src/main/webapp/app/entities/variable/update/variable-form.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import { IVariable, NewVariable } from '../variable.model'; +import { VariableUnitsFormGroup, VariableUnitsFormService } from '../../variable-units/update/variable-units-form.service'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IVariable for edit and NewVariableFormGroupInput for create. + */ +type VariableFormGroupInput = IVariable | PartialWithRequiredKeyOf; + +type VariableFormDefaults = Pick; + +type VariableFormGroupContent = { + id: FormControl; + code: FormControl; + variableIndex: FormControl; + name: FormControl; + description: FormControl; + input: FormControl; + inputMode: FormControl; + output: FormControl; + outputSingle: FormControl; + outputOwner: FormControl; + outputFormula: FormControl; + valueScale: FormControl; + version: FormControl; + baseUnit: FormControl; + variableScope: FormControl; + variableCategory: FormControl; + variableClassType: FormControl; + variableClassMandatory?: FormControl; + hiddenForMain?: FormControl; + variableClassLabel?: FormControl; + variableUnits: FormArray; +}; + +export type VariableFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class VariableFormService { + constructor(private fb: FormBuilder, + private variableUnitsFormService: VariableUnitsFormService) {} + + createVariableFormGroup(variable: VariableFormGroupInput = { id: null }): VariableFormGroup { + const variableRawValue = { + ...this.getFormDefaults(), + ...variable, + }; + + const items = variable?.variableUnits??[]; + + return new FormGroup({ + id: new FormControl( + { value: variableRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(variableRawValue.code, { + validators: [Validators.required, Validators.pattern('^[0-9]*$')], + }), + variableIndex: new FormControl(variableRawValue.variableIndex), + name: new FormControl(variableRawValue.name, { + validators: [Validators.required], + }), + description: new FormControl(variableRawValue.description, { + validators: [Validators.required], + }), + input: new FormControl(variableRawValue.input, { + validators: [Validators.required], + }), + inputMode: new FormControl(variableRawValue.inputMode), + output: new FormControl(variableRawValue.output, { + validators: [Validators.required], + }), + outputSingle: new FormControl(variableRawValue.outputSingle), + outputOwner: new FormControl(variableRawValue.outputOwner), + outputFormula: new FormControl(variableRawValue.outputFormula), + valueScale: new FormControl(variableRawValue.valueScale), + version: new FormControl(variableRawValue.version), + baseUnit: new FormControl(variableRawValue.baseUnit, { + validators: [Validators.required], + }), + variableScope: new FormControl(variableRawValue.variableScope), + variableCategory: new FormControl(variableRawValue.variableCategory), + variableClassType: new FormControl(variableRawValue.variableClassType), + variableClassMandatory: new FormControl(variableRawValue.variableClassMandatory), + hiddenForMain: new FormControl(variableRawValue.hiddenForMain), + variableClassLabel: new FormControl(variableRawValue.variableClassLabel), + variableUnits: new FormArray(items.map(item => this.variableUnitsFormService.createVariableUnitsFormGroup(item))) + }); + } + + getVariable(form: VariableFormGroup): IVariable | NewVariable { + return form.getRawValue() as IVariable | NewVariable; + } + + resetForm(form: VariableFormGroup, variable: VariableFormGroupInput): void { + const variableRawValue = { ...this.getFormDefaults(), ...variable }; + form.reset( + { + ...variableRawValue, + id: { value: variableRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): VariableFormDefaults { + return { + id: null, + input: false, + output: false, + variableClassMandatory: false, + }; + } +} diff --git a/src/main/webapp/app/entities/variable/update/variable-update.component.css b/src/main/webapp/app/entities/variable/update/variable-update.component.css new file mode 100644 index 0000000..a620fa3 --- /dev/null +++ b/src/main/webapp/app/entities/variable/update/variable-update.component.css @@ -0,0 +1,8 @@ +span.help-formula-toggle { + float: right; + cursor: pointer; +} + +#collapseSection .card-body { + margin-bottom: 5px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/variable/update/variable-update.component.html b/src/main/webapp/app/entities/variable/update/variable-update.component.html new file mode 100644 index 0000000..8e0fe5c --- /dev/null +++ b/src/main/webapp/app/entities/variable/update/variable-update.component.html @@ -0,0 +1,529 @@ + +
+
+
+

+ Criar ou editar Variable +

+ +
+ +
+ + @if (true || editForm.controls.id.value == null) { + + } @else { + + } + +
+
+ + @if (true || editForm.controls.id.value == null) { + + } @else { + + } + +
+
+ + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
+ @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.pattern) { + Este campo deve estar dentro do seguinte padrão Code. + } +
+ } +
+
+ + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
+ @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ + +
+
+ + + @if (editForm.get('description')!.invalid && (editForm.get('description')!.dirty || editForm.get('description')!.touched)) { +
+ @if (editForm.get('description')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + +
+ + + + + + +
+
+
+
+ + @if (editForm.get('input')!.invalid && (editForm.get('input')!.dirty || editForm.get('input')!.touched)) { +
+ @if (editForm.get('input')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ +
+
+ + @if (editForm.get('hiddenForMain')!.invalid && (editForm.get('hiddenForMain')!.dirty || editForm.get('hiddenForMain')!.touched)) { +
+ @if (editForm.get('hiddenForMain')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ +
+ + + @if (editForm.get('inputMode')!.invalid && (editForm.get('inputMode')!.dirty || editForm.get('inputMode')!.touched)) { +
+ @if (editForm.get('inputMode')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ +
+
+
+ + @if (editForm.get('output')!.invalid && (editForm.get('output')!.dirty || editForm.get('output')!.touched)) { +
+ @if (editForm.get('output')?.errors?.required) { + O campo é obrigatório. + } +
+ } + + +
+
+ +
+
+ + +
+
+ +
+ + +
+ +
+
+ + + {{ '' // Add fa-icon 'faCaretDown' to /app/config/font-awesome-icons.ts }} + {{ '' // Add fa-icon 'faCaretUp' to /app/config/font-awesome-icons.ts }} + {{ 'resilientApp.variable.outputFormulaHelp' | translate : { default: 'help' } }} + +
+ +
+
+
Formula helper
+

Formula fields provide a powerfull way of configuration and calculation. Use any of SpEL language and funtions.

+

In this context, the following variables are added:

+
    +
  • variable - the Variable
  • +
  • period - the Period
  • +
  • periodVersion - the Period Version
  • +
  • owner - the Owner of the information, this is an Organization level
  • +
+

In this context, the following functions are added:

+
    +
  • fe(EmissionFactorCode) - Searches for the emission factor by its code, and the closest to the Year of the current Period (<=Period.Year).
  • +
  • fe(EmissionFactorCode, Year) - Searches for the emission factor by its code, and the closest to the given Year (<=Year).
  • +
  • in(VariableCode) - Searches for inputs for this variable code, without class restriction. Restricted by the current Period.
  • +
  • in(VariableCode, ClassCode) - Searches for inputs for this variable code, restricted by class. Restricted by the current Period.
  • +
  • in(VariableCode, ClassCode, OwnerCode) - Searches for inputs for this variable code, restricted by class and Owner code. Restricted by the current Period.
  • +
  • NOTE: the ClassCode can be filled with the value "*ALL", this will ignore the class code restriction. Usefull when using the function: in(VariableCode, ClassCode, OwnerCode), to get all values for a OwnerCode without Class restriction.
  • +
  • out(VariableCode) - Searches for calculated outputs for this variable code. Restricted by the current Owner and Period Version.
  • +
  • out(VariableCode, OwnerCode) - Searches for calculated outputs for this variable code, for the given Owner Code. Restricted by the current Period Version.
  • +
  • outByUnit(UnitCode) - Searches for calculated outputs for this unit code. Restricted by the current Owner and Period Version.
  • +
  • outByUnit(UnitCode, OwnerCode) - Searches for calculated outputs for this unit code, for the given Owner Code. Restricted by the current Period Version.
  • +
+
+
+ + + +
+
+ +
+
+ + +
+ +
+ + + @if (editForm.get('variableClassLabel')!.invalid && (editForm.get('variableClassLabel')!.dirty || editForm.get('variableClassLabel')!.touched)) { +
+ @if (editForm.get('variableClassLabel')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+ +
+ + + @if (editForm.get('variableClassMandatory')!.invalid && (editForm.get('variableClassMandatory')!.dirty || editForm.get('variableClassMandatory')!.touched)) { +
+ @if (editForm.get('variableClassMandatory')?.errors?.required) { + O campo é obrigatório. + } +
+ } +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
UnitConverter
{{i+1}} + + + + + +
+
+
+ + +
+ +
+ + + +
+
+
+
diff --git a/src/main/webapp/app/entities/variable/update/variable-update.component.spec.ts b/src/main/webapp/app/entities/variable/update/variable-update.component.spec.ts new file mode 100644 index 0000000..f7dc527 --- /dev/null +++ b/src/main/webapp/app/entities/variable/update/variable-update.component.spec.ts @@ -0,0 +1,242 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of, Subject, from } from 'rxjs'; + +import { IUnit } from 'app/entities/unit/unit.model'; +import { UnitService } from 'app/entities/unit/service/unit.service'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { IVariable } from '../variable.model'; +import { VariableService } from '../service/variable.service'; +import { VariableFormService } from './variable-form.service'; + +import { VariableUpdateComponent } from './variable-update.component'; + +describe('Variable Management Update Component', () => { + let comp: VariableUpdateComponent; + let fixture: ComponentFixture; + let activatedRoute: ActivatedRoute; + let variableFormService: VariableFormService; + let variableService: VariableService; + let unitService: UnitService; + let variableScopeService: VariableScopeService; + let variableCategoryService: VariableCategoryService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, VariableUpdateComponent], + providers: [ + FormBuilder, + { + provide: ActivatedRoute, + useValue: { + params: from([{}]), + }, + }, + ], + }) + .overrideTemplate(VariableUpdateComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(VariableUpdateComponent); + activatedRoute = TestBed.inject(ActivatedRoute); + variableFormService = TestBed.inject(VariableFormService); + variableService = TestBed.inject(VariableService); + unitService = TestBed.inject(UnitService); + variableScopeService = TestBed.inject(VariableScopeService); + variableCategoryService = TestBed.inject(VariableCategoryService); + + comp = fixture.componentInstance; + }); + + describe('ngOnInit', () => { + it('Should call Unit query and add missing value', () => { + const variable: IVariable = { id: 456 }; + const baseUnit: IUnit = { id: 10597 }; + variable.baseUnit = baseUnit; + + const unitCollection: IUnit[] = [{ id: 3695 }]; + jest.spyOn(unitService, 'query').mockReturnValue(of(new HttpResponse({ body: unitCollection }))); + const additionalUnits = [baseUnit]; + const expectedCollection: IUnit[] = [...additionalUnits, ...unitCollection]; + jest.spyOn(unitService, 'addUnitToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ variable }); + comp.ngOnInit(); + + expect(unitService.query).toHaveBeenCalled(); + expect(unitService.addUnitToCollectionIfMissing).toHaveBeenCalledWith( + unitCollection, + ...additionalUnits.map(expect.objectContaining), + ); + expect(comp.unitsSharedCollection).toEqual(expectedCollection); + }); + + it('Should call VariableScope query and add missing value', () => { + const variable: IVariable = { id: 456 }; + const variableScope: IVariableScope = { id: 20996 }; + variable.variableScope = variableScope; + + const variableScopeCollection: IVariableScope[] = [{ id: 8247 }]; + jest.spyOn(variableScopeService, 'query').mockReturnValue(of(new HttpResponse({ body: variableScopeCollection }))); + const additionalVariableScopes = [variableScope]; + const expectedCollection: IVariableScope[] = [...additionalVariableScopes, ...variableScopeCollection]; + jest.spyOn(variableScopeService, 'addVariableScopeToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ variable }); + comp.ngOnInit(); + + expect(variableScopeService.query).toHaveBeenCalled(); + expect(variableScopeService.addVariableScopeToCollectionIfMissing).toHaveBeenCalledWith( + variableScopeCollection, + ...additionalVariableScopes.map(expect.objectContaining), + ); + expect(comp.variableScopesSharedCollection).toEqual(expectedCollection); + }); + + it('Should call VariableCategory query and add missing value', () => { + const variable: IVariable = { id: 456 }; + const variableCategory: IVariableCategory = { id: 29225 }; + variable.variableCategory = variableCategory; + + const variableCategoryCollection: IVariableCategory[] = [{ id: 8235 }]; + jest.spyOn(variableCategoryService, 'query').mockReturnValue(of(new HttpResponse({ body: variableCategoryCollection }))); + const additionalVariableCategories = [variableCategory]; + const expectedCollection: IVariableCategory[] = [...additionalVariableCategories, ...variableCategoryCollection]; + jest.spyOn(variableCategoryService, 'addVariableCategoryToCollectionIfMissing').mockReturnValue(expectedCollection); + + activatedRoute.data = of({ variable }); + comp.ngOnInit(); + + expect(variableCategoryService.query).toHaveBeenCalled(); + expect(variableCategoryService.addVariableCategoryToCollectionIfMissing).toHaveBeenCalledWith( + variableCategoryCollection, + ...additionalVariableCategories.map(expect.objectContaining), + ); + expect(comp.variableCategoriesSharedCollection).toEqual(expectedCollection); + }); + + it('Should update editForm', () => { + const variable: IVariable = { id: 456 }; + const baseUnit: IUnit = { id: 13102 }; + variable.baseUnit = baseUnit; + const variableScope: IVariableScope = { id: 31149 }; + variable.variableScope = variableScope; + const variableCategory: IVariableCategory = { id: 26584 }; + variable.variableCategory = variableCategory; + + activatedRoute.data = of({ variable }); + comp.ngOnInit(); + + expect(comp.unitsSharedCollection).toContain(baseUnit); + expect(comp.variableScopesSharedCollection).toContain(variableScope); + expect(comp.variableCategoriesSharedCollection).toContain(variableCategory); + expect(comp.variable).toEqual(variable); + }); + }); + + describe('save', () => { + it('Should call update service on save for existing entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variable = { id: 123 }; + jest.spyOn(variableFormService, 'getVariable').mockReturnValue(variable); + jest.spyOn(variableService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variable }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variable })); + saveSubject.complete(); + + // THEN + expect(variableFormService.getVariable).toHaveBeenCalled(); + expect(comp.previousState).toHaveBeenCalled(); + expect(variableService.update).toHaveBeenCalledWith(expect.objectContaining(variable)); + expect(comp.isSaving).toEqual(false); + }); + + it('Should call create service on save for new entity', () => { + // GIVEN + const saveSubject = new Subject>(); + const variable = { id: 123 }; + jest.spyOn(variableFormService, 'getVariable').mockReturnValue({ id: null }); + jest.spyOn(variableService, 'create').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variable: null }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.next(new HttpResponse({ body: variable })); + saveSubject.complete(); + + // THEN + expect(variableFormService.getVariable).toHaveBeenCalled(); + expect(variableService.create).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).toHaveBeenCalled(); + }); + + it('Should set isSaving to false on error', () => { + // GIVEN + const saveSubject = new Subject>(); + const variable = { id: 123 }; + jest.spyOn(variableService, 'update').mockReturnValue(saveSubject); + jest.spyOn(comp, 'previousState'); + activatedRoute.data = of({ variable }); + comp.ngOnInit(); + + // WHEN + comp.save(); + expect(comp.isSaving).toEqual(true); + saveSubject.error('This is an error!'); + + // THEN + expect(variableService.update).toHaveBeenCalled(); + expect(comp.isSaving).toEqual(false); + expect(comp.previousState).not.toHaveBeenCalled(); + }); + }); + + describe('Compare relationships', () => { + describe('compareUnit', () => { + it('Should forward to unitService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(unitService, 'compareUnit'); + comp.compareUnit(entity, entity2); + expect(unitService.compareUnit).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareVariableScope', () => { + it('Should forward to variableScopeService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(variableScopeService, 'compareVariableScope'); + comp.compareVariableScope(entity, entity2); + expect(variableScopeService.compareVariableScope).toHaveBeenCalledWith(entity, entity2); + }); + }); + + describe('compareVariableCategory', () => { + it('Should forward to variableCategoryService', () => { + const entity = { id: 123 }; + const entity2 = { id: 456 }; + jest.spyOn(variableCategoryService, 'compareVariableCategory'); + comp.compareVariableCategory(entity, entity2); + expect(variableCategoryService.compareVariableCategory).toHaveBeenCalledWith(entity, entity2); + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/variable/update/variable-update.component.ts b/src/main/webapp/app/entities/variable/update/variable-update.component.ts new file mode 100644 index 0000000..3c65d3f --- /dev/null +++ b/src/main/webapp/app/entities/variable/update/variable-update.component.ts @@ -0,0 +1,429 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, tap, of } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule, FormArray, Validators } from '@angular/forms'; + +import { IUnit } from 'app/entities/unit/unit.model'; +import { UnitService } from 'app/entities/unit/service/unit.service'; +import { IUnitConverter } from 'app/entities/unit-converter/unit-converter.model'; +import { UnitConverterService } from 'app/entities/unit-converter/service/unit-converter.service'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { VariableScopeService } from 'app/entities/variable-scope/service/variable-scope.service'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { VariableCategoryService } from 'app/entities/variable-category/service/variable-category.service'; +import { IVariableClassType } from 'app/entities/variable-class-type/variable-class-type.model'; +import { VariableClassTypeService } from 'app/entities/variable-class-type/service/variable-class-type.service'; +import { InputMode } from 'app/entities/enumerations/input-mode.model'; +import { VariableService } from '../service/variable.service'; +import { IVariable } from '../variable.model'; +import { VariableFormService, VariableFormGroup } from './variable-form.service'; +import { VariableUnitsFormService, VariableUnitsFormGroup } from 'app/entities/variable-units/update/variable-units-form.service'; +import { IVariableUnits } from 'app/entities/variable-units/variable-units.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { OrganizationNature } from 'app/entities/enumerations/organization-nature.model'; +import { CodeMirrorFieldComponent } from 'app/resilient/codemirrorfield/codemirrorfield.component' + +interface KeyValueArray { + key: number; + values: IUnitConverter[]; // or any other type for the inner array elements +} + +@Component({ + standalone: true, + selector: 'jhi-variable-update', + templateUrl: './variable-update.component.html', + styleUrl: './variable-update.component.css', + imports: [SharedModule, FormsModule, ReactiveFormsModule, CodeMirrorFieldComponent], +}) +export class VariableUpdateComponent implements OnInit { + isSaving = false; + variable: IVariable | null = null; + inputModeValues = Object.keys(InputMode); + currentTab: string = 'input'; // default tab + isDisabled = true; + isCollapsed = true; + + unitsSharedCollection: IUnit[] = []; + unitsOfTypeSharedCollection: IUnit[] = []; + variableScopesSharedCollection: IVariableScope[] = []; + variableCategoriesSharedCollection: IVariableCategory[] = []; + variableClassTypesSharedCollection: IVariableClassType[] = []; + organizationsSharedCollection: IOrganization[] = []; + + /* unitConvertersSharedCollection: IUnitConverter[] = []; */ + unitConvertersSharedCollection = {}; + unitConvertersOptions: KeyValueArray[] = []; + + protected variableService = inject(VariableService); + protected variableFormService = inject(VariableFormService); + protected variableUnitsFormService = inject(VariableUnitsFormService); + protected unitService = inject(UnitService); + protected unitConverterService = inject(UnitConverterService); + protected variableScopeService = inject(VariableScopeService); + protected variableCategoryService = inject(VariableCategoryService); + protected variableClassTypeService = inject(VariableClassTypeService); + protected activatedRoute = inject(ActivatedRoute); + protected organizationService = inject(OrganizationService); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: VariableFormGroup = this.variableFormService.createVariableFormGroup(); + + compareUnit = (o1: IUnit | null, o2: IUnit | null): boolean => this.unitService.compareEntity(o1, o2); + compareUnitConverter = (o1: IUnitConverter | null, o2: IUnitConverter | null): boolean => this.unitConverterService.compareEntity(o1, o2); + + compareVariableScope = (o1: IVariableScope | null, o2: IVariableScope | null): boolean => + this.variableScopeService.compareEntity(o1, o2); + + compareVariableCategory = (o1: IVariableCategory | null, o2: IVariableCategory | null): boolean => + this.variableCategoryService.compareEntity(o1, o2); + + compareVariableClassType = (o1: IVariableClassType | null, o2: IVariableClassType | null): boolean => + this.variableClassTypeService.compareEntity(o1, o2); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ variable }) => { + this.variable = variable; + if (variable) { + this.updateForm(variable); + } else { + this.protectBaseUnitSelect(); + this.variableClassTypeChange(); + this.protectInputTab(); + this.protectOutputTab(); + this.protectOutputSingle(); + } + + // Listen to changes to the scope filter property + this.editForm.get('variableScope')?.valueChanges.subscribe((scope) => { + this.editForm.get('variableCategory')?.setValue(null); + this.loadCategoriesOfSelectedScope(scope).subscribe((cats: IVariableCategory[] | null) => { + if (cats) { + this.variableCategoriesSharedCollection = cats; + } + }); + }); + + this.loadRelationshipsOptions(); + }); + + this.editForm.get('variableUnits')?.valueChanges.subscribe(changes=> { + this.protectBaseUnitSelect(); + }) + + this.editForm.get('variableClassType')?.valueChanges.subscribe((selectedItem) => { + this.variableClassTypeChange(); + }); + + this.editForm.get('input')?.valueChanges.subscribe((value) => { + this.protectInputTab(); + }); + + this.editForm.get('output')?.valueChanges.subscribe((value) => { + this.protectOutputTab(); + }); + + this.editForm.get('outputSingle')?.valueChanges.subscribe((value) => { + this.protectOutputSingle(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const variable = this.variableFormService.getVariable(this.editForm); + if (variable.id !== null) { + this.subscribeToSaveResponse(this.variableService.update(variable)); + } else { + this.subscribeToSaveResponse(this.variableService.create(variable)); + } + } + + setTab(tabId: string): void { + this.currentTab = tabId; + } + + /* Nested collection methods : VariableUnits's */ + get variableUnits(): FormArray { + return this.editForm.get('variableUnits') as FormArray; + } + + addVariableUnit(varUnit: IVariableUnits | null): void { + // Create new instance of the VariableUnit form group + let variableUnitsFormGroup: VariableUnitsFormGroup; + if (varUnit) { + variableUnitsFormGroup = this.variableUnitsFormService.createVariableUnitsFormGroup(varUnit); + this.loadVariableConvertersForUnit(variableUnitsFormGroup, varUnit.unit, false); + } else { + variableUnitsFormGroup = this.variableUnitsFormService.createVariableUnitsFormGroup(); + } + + // Listen to changes to the VariableUnit.unit property + variableUnitsFormGroup.get('unit')?.valueChanges.subscribe((unit) => { + this.loadVariableConvertersForUnit(variableUnitsFormGroup, unit, true); + + // Set validator + const baseUnit = this.editForm.get('baseUnit')?.value; + const unitConverterControl = variableUnitsFormGroup.get('unitConverter'); + if (baseUnit && unit && unitConverterControl) { + if (baseUnit.unitType?.id === unit.unitType?.id) { + unitConverterControl.clearValidators(); + } else { + unitConverterControl.setValidators([Validators.required]); + } + + // Update the validity of the control + unitConverterControl.updateValueAndValidity(); + } + }); + + // Add it to the FormArray + this.variableUnits.push( variableUnitsFormGroup ); + } + + removeVariableUnit(index: number): void { + this.variableUnits.removeAt(index); + } + + // Load subcategory options based on the selected category + loadVariableConvertersForUnit(variableUnitsFormGroup: VariableUnitsFormGroup, unit: Pick | null | undefined, clearUnitConverter: boolean): void { + const baseUnit = this.editForm.get('baseUnit')?.value; + const unitConverterControl = variableUnitsFormGroup.get('unitConverter'); + + if (unitConverterControl) { + if (clearUnitConverter) { unitConverterControl.setValue(null); } + + if (baseUnit && unit) { + // Enable control for editing + unitConverterControl.enable(); + + // Call server for UnitConverter loading + this.unitConverterService + .queryByFromAndTo(unit.id, baseUnit.id) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((unitConverters: IUnitConverter[]) => { + variableUnitsFormGroup.get('unitConverterOptions')?.setValue( unitConverters ); // Save options in FormGroup + }); + } else { + unitConverterControl.disable(); + } + } + } + + protected variableClassTypeChange(): void { + const selectedValue = this.editForm.get('variableClassType')?.value; + + if (selectedValue) { + this.editForm.get('variableClassLabel')?.enable(); + this.editForm.get('variableClassMandatory')?.enable(); + } else { + this.editForm.get('variableClassLabel')?.setValue(null); + this.editForm.get('variableClassLabel')?.disable(); + this.editForm.get('variableClassMandatory')?.setValue(null); + this.editForm.get('variableClassMandatory')?.disable(); + } + } + + protected protectInputTab(): void { + const input = this.editForm.get("input")?.value; + if (input) { + this.editForm.get('inputMode')?.enable(); + this.editForm.get('inputMode')?.setValidators([Validators.required]); + this.editForm.get('hiddenForMain')?.enable(); + } else { + this.editForm.get('inputMode')?.setValue(null); + this.editForm.get('inputMode')?.disable(); + this.editForm.get('inputMode')?.clearValidators(); + this.editForm.get('hiddenForMain')?.disable(); + } + this.editForm.get('inputMode')?.updateValueAndValidity(); + } + + protected protectOutputTab(): void { + const output = this.editForm.get("output")?.value; + if (output) { + this.editForm.get('outputSingle')?.enable(); + this.editForm.get('outputFormula')?.enable(); + this.editForm.get('outputFormula')?.setValidators([Validators.required]); + } else { + this.editForm.get('outputFormula')?.setValue(null); + this.editForm.get('outputFormula')?.disable(); + this.editForm.get('outputSingle')?.setValue(false); + this.editForm.get('outputSingle')?.disable(); + this.editForm.get('outputFormula')?.clearValidators(); + } + this.editForm.get('outputFormula')?.updateValueAndValidity(); + } + + protected protectOutputSingle(): void { + const outputSingle = this.editForm.get("outputSingle")?.value; + if (outputSingle) { + this.editForm.get('outputOwner')?.enable(); + this.editForm.get('outputOwner')?.setValidators([Validators.required]); + } else { + this.editForm.get('outputOwner')?.setValue(null); + this.editForm.get('outputOwner')?.disable(); + this.editForm.get('outputOwner')?.clearValidators(); + } + this.editForm.get('outputOwner')?.updateValueAndValidity(); + } + + protected protectBaseUnitSelect(): void { + const formArrayLength = this.variableUnits.length; + if (formArrayLength > 0) { + this.editForm.get('baseUnit')?.disable(); + } else { + this.editForm.get('baseUnit')?.enable(); + } + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(variable: IVariable): void { + this.variable = variable; + this.variableFormService.resetForm(this.editForm, variable); + + // variable.variableUnits FormArray + variable.variableUnits?.forEach(varUnit => { + this.addVariableUnit(varUnit); + }); + + this.unitsSharedCollection = this.unitService.addEntityToCollectionIfMissing(this.unitsSharedCollection, variable.baseUnit); + + this.variableScopesSharedCollection = this.variableScopeService.addEntityToCollectionIfMissing( + this.variableScopesSharedCollection, + variable.variableScope, + ); + + this.organizationsSharedCollection = this.organizationService.addEntityToCollectionIfMissing( + this.organizationsSharedCollection, + variable.outputOwner, + ); + + this.loadCategoriesOfSelectedScope(variable.variableScope).subscribe((cats: IVariableCategory[] | null) => { + if (cats) { + this.variableCategoriesSharedCollection = cats; + } + + this.variableCategoriesSharedCollection = this.variableCategoryService.addEntityToCollectionIfMissing( + this.variableCategoriesSharedCollection, + variable.variableCategory, + ); + }); + + this.protectBaseUnitSelect(); + this.variableClassTypeChange(); + this.protectInputTab(); + this.protectOutputTab(); + this.protectOutputSingle(); + } + + protected loadUnitsOfTypeSharedCollection(baseUnitId: number|null): void { + if (baseUnitId) { + this.unitService + .queryByType(baseUnitId) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((units: IUnit[]) => (this.unitsOfTypeSharedCollection = units)); + } else { + this.unitsOfTypeSharedCollection = []; + } + } + + protected loadCategoriesOfSelectedScope(scope: IVariableScope|null|undefined): Observable { + if (scope) { + /* + this.variableCategoryService + .queryByScopeForData(scope.id) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((cats: IVariableCategory[]) => (this.variableCategoriesSharedCollection = cats)); + */ + return this.variableCategoryService + .queryByScope(scope.id) + .pipe(map((res: HttpResponse) => res.body ?? [])); + } else { + // this.variableCategoriesSharedCollection = []; + return of(null).pipe( + tap(() => { + this.variableCategoriesSharedCollection = []; + }) + ); + } + } + + protected loadRelationshipsOptions(): void { + this.unitService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe(map((units: IUnit[]) => this.unitService.addEntityToCollectionIfMissing(units, this.variable?.baseUnit))) + .subscribe((units: IUnit[]) => (this.unitsSharedCollection = units)); + + this.unitConverterService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + /* .pipe(map((units: IUnit[]) => this.unitService.addEntityToCollectionIfMissing(units, this.variable?.baseUnit))) */ + .subscribe((unitConverters: IUnitConverter[]) => (this.unitConvertersSharedCollection = unitConverters)); + + this.variableScopeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((variableScopes: IVariableScope[]) => + this.variableScopeService.addEntityToCollectionIfMissing(variableScopes, this.variable?.variableScope), + ), + ) + .subscribe((variableScopes: IVariableScope[]) => (this.variableScopesSharedCollection = variableScopes)); + + this.variableClassTypeService + .query() + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((variableClassTypes: IVariableClassType[]) => + this.variableClassTypeService.addEntityToCollectionIfMissing( + variableClassTypes, + this.variable?.variableClassType, + ), + ), + ) + .subscribe((variableClassTypes: IVariableClassType[]) => (this.variableClassTypesSharedCollection = variableClassTypes)); + + this.organizationService + .queryByNature(OrganizationNature.ORGANIZATION) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .pipe( + map((organizations: IOrganization[]) => + this.organizationService.addEntityToCollectionIfMissing(organizations, this.variable?.outputOwner), + ), + ) + .subscribe((organizations: IOrganization[]) => { + this.organizationsSharedCollection = organizations; + }); + } +} + diff --git a/src/main/webapp/app/entities/variable/variable.model.ts b/src/main/webapp/app/entities/variable/variable.model.ts new file mode 100644 index 0000000..aafdab1 --- /dev/null +++ b/src/main/webapp/app/entities/variable/variable.model.ts @@ -0,0 +1,47 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; + +import { IUnit } from 'app/entities/unit/unit.model'; +import { IVariableScope } from 'app/entities/variable-scope/variable-scope.model'; +import { IVariableCategory } from 'app/entities/variable-category/variable-category.model'; +import { IVariableClassType } from 'app/entities/variable-class-type/variable-class-type.model'; +import { IVariableUnits } from 'app/entities/variable-units/variable-units.model'; +import { InputMode } from 'app/entities/enumerations/input-mode.model'; +import { IOrganization } from 'app/entities/organization/organization.model'; + +export interface IVariable extends IResilientEntity { + code?: string | null; + variableIndex?: number | null; + name?: string | null; + description?: string | null; + input?: boolean | null; + inputMode?: keyof typeof InputMode | null; + output?: boolean | null; + outputSingle?: boolean | null; + outputOwner?: IOrganization | null; + outputFormula?: string | null; + valueScale?: number | null; + baseUnit?: Pick | null; + variableScope?: Pick | null; + variableCategory?: Pick | null; + variableClassType?: Pick | null; + variableClassMandatory?: boolean | null; + variableClassLabel?: string | null; + variableUnits?: IVariableUnits[] | null; + hiddenForMain?: boolean | null; +} + +export type NewVariable = Omit & { id: null }; + +export type SearchVariable = Omit< + IVariable, + // Remove unwanted properties in search + 'id' | 'variableUnits' | 'variableClassType' | 'variableCategory' | 'variableScope' | 'outputOwner'> + & + // Add specific properties for search + { + id?: number | null; + variableScopeId?: number | null; + variableCategoryId?: number | null; + variableClassTypeId?: number | null; + }; + diff --git a/src/main/webapp/app/entities/variable/variable.routes.ts b/src/main/webapp/app/entities/variable/variable.routes.ts new file mode 100644 index 0000000..dbfeb1e --- /dev/null +++ b/src/main/webapp/app/entities/variable/variable.routes.ts @@ -0,0 +1,70 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; +import { VariableComponent } from './list/variable.component'; +import { VariableDetailComponent } from './detail/variable-detail.component'; +import { VariableUpdateComponent } from './update/variable-update.component'; +import VariableResolve from './route/variable-routing-resolve.service'; +import { SecurityAction } from 'app/security/security-action.model'; +import { Resources } from 'app/security/resources.model'; +import { VariableService } from './service/variable.service'; + +const variableRoute: Routes = [ + { + path: '', + component: VariableComponent, + canActivate: [UserRouteAccessService], + data: { + defaultSort: 'id,' + ASC, + action: SecurityAction.READ, + service: VariableService, + resource: Resources.VARIABLE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/view', + component: VariableDetailComponent, + resolve: { + variable: VariableResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.READ, + service: VariableService, + resource: Resources.VARIABLE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: 'new', + component: VariableUpdateComponent, + resolve: { + variable: VariableResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.CREATE, + service: VariableService, + resource: Resources.VARIABLE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, + { + path: ':id/edit', + component: VariableUpdateComponent, + resolve: { + variable: VariableResolve, + }, + canActivate: [UserRouteAccessService], + data: { + action: SecurityAction.UPDATE, + service: VariableService, + resource: Resources.VARIABLE, + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER'], + } + }, +]; + +export default variableRoute; diff --git a/src/main/webapp/app/entities/variable/variable.test-samples.ts b/src/main/webapp/app/entities/variable/variable.test-samples.ts new file mode 100644 index 0000000..cbb1ddc --- /dev/null +++ b/src/main/webapp/app/entities/variable/variable.test-samples.ts @@ -0,0 +1,54 @@ +import { IVariable, NewVariable } from './variable.model'; + +export const sampleWithRequiredData: IVariable = { + id: 15762, + code: undefined, + variableIndex: 1848, + name: 'bet huzzah where', + description: 'violently', + input: false, + inputMode: 'DATAFILE', + output: true, +}; + +export const sampleWithPartialData: IVariable = { + id: 12212, + code: '4890', + variableIndex: 19981, + name: 'pastel', + description: 'lie tackle', + input: true, + inputMode: 'INTEGRATION', + output: true, + outputFormula: 'or standardization', + version: 30421, +}; + +export const sampleWithFullData: IVariable = { + id: 24329, + code: '180', + variableIndex: 7006, + name: 'in versus', + description: 'duh', + input: true, + inputMode: 'INTEGRATION', + output: false, + outputFormula: 'meld during oh', + version: 5802, +}; + +export const sampleWithNewData: NewVariable = { + code: undefined, + variableIndex: 25922, + name: 'yet mmm procreate', + description: 'round dislocate uncomfortable', + input: true, + inputMode: 'DATAFILE', + output: true, + id: null, +}; + +Object.freeze(sampleWithNewData); +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/src/main/webapp/app/home/home.component.html b/src/main/webapp/app/home/home.component.html new file mode 100644 index 0000000..aeedce6 --- /dev/null +++ b/src/main/webapp/app/home/home.component.html @@ -0,0 +1,10 @@ + +
\ No newline at end of file diff --git a/src/main/webapp/app/home/home.component.scss b/src/main/webapp/app/home/home.component.scss new file mode 100644 index 0000000..0a25df3 --- /dev/null +++ b/src/main/webapp/app/home/home.component.scss @@ -0,0 +1,25 @@ +/* ========================================================================== +Main page styles +========================================================================== */ + +.hipster { + display: inline-block; + width: 347px; + height: 497px; + /* background: url('../../content/images/jhipster_family_member_1.svg') no-repeat center top; */ + background: url('../../content/images/inNova_logo.png') no-repeat left center; + background-size: contain; +} + +/* wait autoprefixer update to allow simple generation of high pixel density media query */ +@media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (-moz-min-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + .hipster { + /* background: url('../../content/images/jhipster_family_member_1.svg') no-repeat center top; */ + background: url('../../content/images/inNova_logo.png') no-repeat left center; + background-size: contain; + } +} diff --git a/src/main/webapp/app/home/home.component.spec.ts b/src/main/webapp/app/home/home.component.spec.ts new file mode 100644 index 0000000..b441a13 --- /dev/null +++ b/src/main/webapp/app/home/home.component.spec.ts @@ -0,0 +1,110 @@ +jest.mock('app/core/auth/account.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of, Subject } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; + +import HomeComponent from './home.component'; + +describe('Home Component', () => { + let comp: HomeComponent; + let fixture: ComponentFixture; + let mockAccountService: AccountService; + let mockRouter: Router; + const account: Account = { + activated: true, + authorities: [], + email: '', + firstName: null, + langKey: '', + lastName: null, + login: 'login', + imageUrl: null, + }; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HomeComponent], + providers: [AccountService], + }) + .overrideTemplate(HomeComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + comp = fixture.componentInstance; + mockAccountService = TestBed.inject(AccountService); + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + }); + + describe('ngOnInit', () => { + it('Should synchronize account variable with current account', () => { + // GIVEN + const authenticationState = new Subject(); + mockAccountService.getAuthenticationState = jest.fn(() => authenticationState.asObservable()); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account()).toBeNull(); + + // WHEN + authenticationState.next(account); + + // THEN + expect(comp.account()).toEqual(account); + + // WHEN + authenticationState.next(null); + + // THEN + expect(comp.account()).toBeNull(); + }); + }); + + describe('login', () => { + it('Should navigate to /login on login', () => { + // WHEN + comp.login(); + + // THEN + expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']); + }); + }); + + describe('ngOnDestroy', () => { + it('Should destroy authentication state subscription on component destroy', () => { + // GIVEN + const authenticationState = new Subject(); + mockAccountService.getAuthenticationState = jest.fn(() => authenticationState.asObservable()); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account()).toBeNull(); + + // WHEN + authenticationState.next(account); + + // THEN + expect(comp.account()).toEqual(account); + + // WHEN + comp.ngOnDestroy(); + authenticationState.next(null); + + // THEN + expect(comp.account()).toEqual(account); + }); + }); +}); diff --git a/src/main/webapp/app/home/home.component.ts b/src/main/webapp/app/home/home.component.ts new file mode 100644 index 0000000..dfc446d --- /dev/null +++ b/src/main/webapp/app/home/home.component.ts @@ -0,0 +1,60 @@ +import { Component, inject, signal, OnDestroy, OnInit } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +import SharedModule from 'app/shared/shared.module'; +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; +import { HomeService } from './service/home.service'; + +@Component({ + standalone: true, + selector: 'jhi-home', + templateUrl: './home.component.html', + styleUrl: './home.component.scss', + imports: [SharedModule, RouterModule], +}) +export default class HomeComponent implements OnDestroy, OnInit { + account = signal(null); + htmlPage: SafeHtml = ""; + + private readonly destroy$ = new Subject(); + + private homeService = inject(HomeService); + private accountService = inject(AccountService); + private router = inject(Router); + private sanitizer = inject(DomSanitizer); + + ngOnInit(): void { + this.accountService + .getAuthenticationState() + .pipe(takeUntil(this.destroy$)) + .subscribe(account => { + this.htmlPage = this.sanitizer.bypassSecurityTrustHtml(""); + this.account.set(account); + + // Load HOME PAGE + this.homeService.homePage().subscribe((html) => { + const rawHtml = html.body ?? ""; + this.htmlPage = this.sanitizer.bypassSecurityTrustHtml(rawHtml); + }); + }); + + // Load HOME PAGE + this.homeService.homePage().subscribe((html) => { + const rawHtml = html.body ?? ""; + this.htmlPage = this.sanitizer.bypassSecurityTrustHtml(rawHtml); + }); + + } + + login(): void { + this.router.navigate(['/login']); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/src/main/webapp/app/home/service/home.service.ts b/src/main/webapp/app/home/service/home.service.ts new file mode 100644 index 0000000..b2eb7ad --- /dev/null +++ b/src/main/webapp/app/home/service/home.service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { HttpClient, HttpResponse } from '@angular/common/http'; + +@Injectable({ providedIn: 'root' }) +export class HomeService { + protected http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/home'); + + homePage(): Observable> { + return this.http.get(`${this.resourceUrl}/page`, { + observe: 'response', + responseType: 'text' + }) as Observable>; + } + +} diff --git a/src/main/webapp/app/layouts/base-detail/base-detail.component.html b/src/main/webapp/app/layouts/base-detail/base-detail.component.html new file mode 100644 index 0000000..faa2b7e --- /dev/null +++ b/src/main/webapp/app/layouts/base-detail/base-detail.component.html @@ -0,0 +1,23 @@ +
+
+ @if (activityDomain) { +
+

+ @if (_titleI18N) {} + @else if (_title) {{{ _title }}} + @else {__ no title __} +

+ +
+ + + + + + + + +
+ } +
+
\ No newline at end of file diff --git a/src/main/webapp/app/layouts/base-detail/base-detail.component.ts b/src/main/webapp/app/layouts/base-detail/base-detail.component.ts new file mode 100644 index 0000000..b84a958 --- /dev/null +++ b/src/main/webapp/app/layouts/base-detail/base-detail.component.ts @@ -0,0 +1,44 @@ +// base-layout.component.ts +import { Component, Input } from '@angular/core'; +import { IActivityDomain } from 'app/entities/activity/activity.model'; +import { ResilientActivityProgressComponent } from 'app/resilient/resilient-activity/progress/resilient-activity-progress.component'; + +import SharedModule from 'app/shared/shared.module'; + +@Component({ + standalone: true, // Mark as standalone + selector: 'base-detail-layout', + templateUrl: './base-detail.component.html', + styles: [` + .content { padding: 1rem; } + `], + imports: [ + SharedModule, + ResilientActivityProgressComponent + ] +}) +export class BaseDetailLayoutComponent { + // Use [activityDomain]="entity()" to enable template domain values + @Input() activityDomain!: IActivityDomain | null; // The "!" makes this NOT optional. An exception will happen at runtime if not provided + + /* + _activityDomain: IActivityDomain | null = null; + @Input('activityDomain') set activityDomain(value: IActivityDomain | null) { + this._activityDomain = value; + } + */ + + // Use [title.text]="'My Title'" to provide the text of the title + _title: string | undefined; + @Input('title.text') set titleText(value: string) { + this._title = value; + console.log('title.text set to:', value); + } + + // Use [title.translate]="app.entity.title" to provide the key for translation + _titleI18N: string | undefined; + @Input('title.translate') set titleTranslate(value: string) { + this._titleI18N = value; + console.log('title.translate set to:', value); + } +} \ No newline at end of file diff --git a/src/main/webapp/app/layouts/error/error.component.html b/src/main/webapp/app/layouts/error/error.component.html new file mode 100644 index 0000000..49e56f7 --- /dev/null +++ b/src/main/webapp/app/layouts/error/error.component.html @@ -0,0 +1,15 @@ +
+
+
+ +
+ +
+

Página de erro!

+ + @if (errorMessage()) { +
{{ errorMessage() }}
+ } +
+
+
diff --git a/src/main/webapp/app/layouts/error/error.component.ts b/src/main/webapp/app/layouts/error/error.component.ts new file mode 100644 index 0000000..cb98a98 --- /dev/null +++ b/src/main/webapp/app/layouts/error/error.component.ts @@ -0,0 +1,45 @@ +import { Component, inject, signal, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import SharedModule from 'app/shared/shared.module'; + +@Component({ + standalone: true, + selector: 'jhi-error', + templateUrl: './error.component.html', + imports: [SharedModule], +}) +export default class ErrorComponent implements OnInit, OnDestroy { + errorMessage = signal(undefined); + errorKey?: string; + langChangeSubscription?: Subscription; + + private translateService = inject(TranslateService); + private route = inject(ActivatedRoute); + + ngOnInit(): void { + this.route.data.subscribe(routeData => { + if (routeData.errorMessage) { + this.errorKey = routeData.errorMessage; + this.getErrorMessageTranslation(); + this.langChangeSubscription = this.translateService.onLangChange.subscribe(() => this.getErrorMessageTranslation()); + } + }); + } + + ngOnDestroy(): void { + if (this.langChangeSubscription) { + this.langChangeSubscription.unsubscribe(); + } + } + + private getErrorMessageTranslation(): void { + this.errorMessage.set(''); + if (this.errorKey) { + this.translateService.get(this.errorKey).subscribe(translatedErrorMessage => { + this.errorMessage.set(translatedErrorMessage); + }); + } + } +} diff --git a/src/main/webapp/app/layouts/error/error.route.ts b/src/main/webapp/app/layouts/error/error.route.ts new file mode 100644 index 0000000..85f911b --- /dev/null +++ b/src/main/webapp/app/layouts/error/error.route.ts @@ -0,0 +1,31 @@ +import { Routes } from '@angular/router'; + +import ErrorComponent from './error.component'; + +export const errorRoute: Routes = [ + { + path: 'error', + component: ErrorComponent, + title: 'error.title', + }, + { + path: 'accessdenied', + component: ErrorComponent, + data: { + errorMessage: 'error.http.403', + }, + title: 'error.title', + }, + { + path: '404', + component: ErrorComponent, + data: { + errorMessage: 'error.http.404', + }, + title: 'error.title', + }, + { + path: '**', + redirectTo: '/404', + }, +]; diff --git a/src/main/webapp/app/layouts/footer/footer.component.html b/src/main/webapp/app/layouts/footer/footer.component.html new file mode 100644 index 0000000..f96063d --- /dev/null +++ b/src/main/webapp/app/layouts/footer/footer.component.html @@ -0,0 +1,6 @@ + diff --git a/src/main/webapp/app/layouts/footer/footer.component.ts b/src/main/webapp/app/layouts/footer/footer.component.ts new file mode 100644 index 0000000..9326a71 --- /dev/null +++ b/src/main/webapp/app/layouts/footer/footer.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { TranslateDirective } from 'app/shared/language'; + +@Component({ + standalone: true, + selector: 'jhi-footer', + templateUrl: './footer.component.html', + imports: [TranslateDirective], +}) +export default class FooterComponent {} diff --git a/src/main/webapp/app/layouts/main/banner/banner.component.html b/src/main/webapp/app/layouts/main/banner/banner.component.html new file mode 100644 index 0000000..49ea24c --- /dev/null +++ b/src/main/webapp/app/layouts/main/banner/banner.component.html @@ -0,0 +1,6 @@ + +
+
+ Route Zero +
+
\ No newline at end of file diff --git a/src/main/webapp/app/layouts/main/banner/banner.component.ts b/src/main/webapp/app/layouts/main/banner/banner.component.ts new file mode 100644 index 0000000..6d3ce95 --- /dev/null +++ b/src/main/webapp/app/layouts/main/banner/banner.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'jhi-banner', + templateUrl: './banner.component.html', +}) +export class BannerComponent {} diff --git a/src/main/webapp/app/layouts/main/main.component.css b/src/main/webapp/app/layouts/main/main.component.css new file mode 100644 index 0000000..6a33819 --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.component.css @@ -0,0 +1,12 @@ +.main-component-banner { + display: none; +} + +/* + ::ng-deep is needed because .authenticated doesn't belong to main.component, its in the tag + ViewEncapsulation prevented this to work. By using ::ng-deep, Angular is forced to evaluate this OUTSIDE + ViewEncapsulation +*/ +::ng-deep .authenticated .main-component-banner { + display: block; +} \ No newline at end of file diff --git a/src/main/webapp/app/layouts/main/main.component.html b/src/main/webapp/app/layouts/main/main.component.html new file mode 100644 index 0000000..6ea7c70 --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.component.html @@ -0,0 +1,15 @@ + + +
+ +
+ + + +
+
+ +
+ + +
diff --git a/src/main/webapp/app/layouts/main/main.component.spec.ts b/src/main/webapp/app/layouts/main/main.component.spec.ts new file mode 100644 index 0000000..f240751 --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.component.spec.ts @@ -0,0 +1,230 @@ +jest.mock('app/core/auth/account.service'); + +import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { Router, TitleStrategy } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { DOCUMENT } from '@angular/common'; +import { Component, NgZone } from '@angular/core'; +import { of } from 'rxjs'; +import { TranslateModule, TranslateService, LangChangeEvent } from '@ngx-translate/core'; + +import { AccountService } from 'app/core/auth/account.service'; + +import { AppPageTitleStrategy } from 'app/app-page-title-strategy'; +import MainComponent from './main.component'; + +describe('MainComponent', () => { + let comp: MainComponent; + let fixture: ComponentFixture; + let titleService: Title; + let translateService: TranslateService; + let mockAccountService: AccountService; + let ngZone: NgZone; + const routerState: any = { snapshot: { root: { data: {} } } }; + let router: Router; + let document: Document; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), MainComponent], + providers: [Title, AccountService, { provide: TitleStrategy, useClass: AppPageTitleStrategy }], + }) + .overrideTemplate(MainComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MainComponent); + comp = fixture.componentInstance; + titleService = TestBed.inject(Title); + translateService = TestBed.inject(TranslateService); + mockAccountService = TestBed.inject(AccountService); + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + ngZone = TestBed.inject(NgZone); + router = TestBed.inject(Router); + document = TestBed.inject(DOCUMENT); + }); + + describe('page title', () => { + const defaultPageTitle = 'global.title'; + const parentRoutePageTitle = 'parentTitle'; + const childRoutePageTitle = 'childTitle'; + const langChangeEvent: LangChangeEvent = { lang: 'pt-pt', translations: null }; + + beforeEach(() => { + routerState.snapshot.root = { data: {} }; + jest.spyOn(translateService, 'get').mockImplementation((key: string | string[]) => of(`${key as string} translated`)); + translateService.currentLang = 'pt-pt'; + jest.spyOn(titleService, 'setTitle'); + comp.ngOnInit(); + }); + + describe('navigation end', () => { + it('should set page title to default title if pageTitle is missing on routes', fakeAsync(() => { + // WHEN + ngZone.run(() => router.navigateByUrl('')); + tick(); + + // THEN + expect(document.title).toBe(defaultPageTitle + ' translated'); + })); + + it('should set page title to root route pageTitle if there is no child routes', fakeAsync(() => { + // GIVEN + router.resetConfig([{ path: '', title: parentRoutePageTitle, component: BlankComponent }]); + + // WHEN + ngZone.run(() => router.navigateByUrl('')); + tick(); + + // THEN + expect(document.title).toBe(parentRoutePageTitle + ' translated'); + })); + + it('should set page title to child route pageTitle if child routes exist and pageTitle is set for child route', fakeAsync(() => { + // GIVEN + router.resetConfig([ + { + path: 'home', + title: parentRoutePageTitle, + children: [{ path: '', title: childRoutePageTitle, component: BlankComponent }], + }, + ]); + + // WHEN + ngZone.run(() => router.navigateByUrl('home')); + tick(); + + // THEN + expect(document.title).toBe(childRoutePageTitle + ' translated'); + })); + + it('should set page title to parent route pageTitle if child routes exists but pageTitle is not set for child route data', fakeAsync(() => { + // GIVEN + router.resetConfig([ + { + path: 'home', + title: parentRoutePageTitle, + children: [{ path: '', component: BlankComponent }], + }, + ]); + + // WHEN + ngZone.run(() => router.navigateByUrl('home')); + tick(); + + // THEN + expect(document.title).toBe(parentRoutePageTitle + ' translated'); + })); + }); + + describe('language change', () => { + it('should set page title to default title if pageTitle is missing on routes', () => { + // WHEN + translateService.onLangChange.emit(langChangeEvent); + + // THEN + expect(document.title).toBe(defaultPageTitle + ' translated'); + }); + + it('should set page title to root route pageTitle if there is no child routes', fakeAsync(() => { + // GIVEN + routerState.snapshot.root.data = { pageTitle: parentRoutePageTitle }; + router.resetConfig([{ path: '', title: parentRoutePageTitle, component: BlankComponent }]); + + // WHEN + ngZone.run(() => router.navigateByUrl('')); + tick(); + + // THEN + expect(document.title).toBe(parentRoutePageTitle + ' translated'); + + // GIVEN + document.title = 'other title'; + + // WHEN + translateService.onLangChange.emit(langChangeEvent); + + // THEN + expect(document.title).toBe(parentRoutePageTitle + ' translated'); + })); + + it('should set page title to child route pageTitle if child routes exist and pageTitle is set for child route', fakeAsync(() => { + // GIVEN + router.resetConfig([ + { + path: 'home', + title: parentRoutePageTitle, + children: [{ path: '', title: childRoutePageTitle, component: BlankComponent }], + }, + ]); + + // WHEN + ngZone.run(() => router.navigateByUrl('home')); + tick(); + + // THEN + expect(document.title).toBe(childRoutePageTitle + ' translated'); + + // GIVEN + document.title = 'other title'; + + // WHEN + translateService.onLangChange.emit(langChangeEvent); + + // THEN + expect(document.title).toBe(childRoutePageTitle + ' translated'); + })); + + it('should set page title to parent route pageTitle if child routes exists but pageTitle is not set for child route data', fakeAsync(() => { + // GIVEN + router.resetConfig([ + { + path: 'home', + title: parentRoutePageTitle, + children: [{ path: '', component: BlankComponent }], + }, + ]); + + // WHEN + ngZone.run(() => router.navigateByUrl('home')); + tick(); + + // THEN + expect(document.title).toBe(parentRoutePageTitle + ' translated'); + + // GIVEN + document.title = 'other title'; + + // WHEN + translateService.onLangChange.emit(langChangeEvent); + + // THEN + expect(document.title).toBe(parentRoutePageTitle + ' translated'); + })); + }); + }); + + describe('page language attribute', () => { + it('should change page language attribute on language change', () => { + // GIVEN + comp.ngOnInit(); + + // WHEN + translateService.onLangChange.emit({ lang: 'lang1', translations: null }); + + // THEN + expect(document.querySelector('html')?.getAttribute('lang')).toEqual('lang1'); + + // WHEN + translateService.onLangChange.emit({ lang: 'lang2', translations: null }); + + // THEN + expect(document.querySelector('html')?.getAttribute('lang')).toEqual('lang2'); + }); + }); +}); + +@Component({ template: '' }) +export class BlankComponent {} diff --git a/src/main/webapp/app/layouts/main/main.component.ts b/src/main/webapp/app/layouts/main/main.component.ts new file mode 100644 index 0000000..bf8109d --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.component.ts @@ -0,0 +1,61 @@ +import { Component, inject, OnInit, RendererFactory2, Renderer2 } from '@angular/core'; +import { RouterOutlet, Router, NavigationEnd } from '@angular/router'; +import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; +import dayjs from 'dayjs/esm'; +import { filter } from 'rxjs/operators'; +import { CommonModule } from '@angular/common'; // Add this! + +import { AccountService } from 'app/core/auth/account.service'; +import { AppPageTitleStrategy } from 'app/app-page-title-strategy'; +import FooterComponent from '../footer/footer.component'; +import PageRibbonComponent from '../profiles/page-ribbon.component'; +import { BannerComponent } from './banner/banner.component'; + +@Component({ + standalone: true, + selector: 'jhi-main', + templateUrl: './main.component.html', + styleUrl: './main.component.css', + providers: [AppPageTitleStrategy], + imports: [RouterOutlet, FooterComponent, PageRibbonComponent, CommonModule, BannerComponent], +}) +export default class MainComponent implements OnInit { + private renderer: Renderer2; + + private router = inject(Router); + private appPageTitleStrategy = inject(AppPageTitleStrategy); + private accountService = inject(AccountService); + private translateService = inject(TranslateService); + private rootRenderer = inject(RendererFactory2); + + showBanner = false; + + constructor() { + this.renderer = this.rootRenderer.createRenderer(document.querySelector('html'), null); + } + + ngOnInit(): void { + // try to log in automatically + this.accountService.identity().subscribe(); + + this.translateService.onLangChange.subscribe((langChangeEvent: LangChangeEvent) => { + this.appPageTitleStrategy.updateTitle(this.router.routerState.snapshot); + dayjs.locale(langChangeEvent.lang); + this.renderer.setAttribute(document.querySelector('html'), 'lang', langChangeEvent.lang); + }); + + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe((event) => { + const navEndEvent = event as NavigationEnd; // Explicitly tell TypeScript + const isLoggedIn = this.accountService.isAuthenticated(); + + // List of routes where the banner should be shown + const bannerRoutes = ['/']; + + // Check if current route matches one of the bannerRoutes + // For this to work, CSS was needed. See main.component.css + this.showBanner = bannerRoutes.some(route => this.router.url === route); + }); + } +} diff --git a/src/main/webapp/app/layouts/navbar/active-menu.directive.ts b/src/main/webapp/app/layouts/navbar/active-menu.directive.ts new file mode 100644 index 0000000..c02595f --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/active-menu.directive.ts @@ -0,0 +1,30 @@ +import { Directive, OnInit, ElementRef, Renderer2, inject, input } from '@angular/core'; +import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; + +@Directive({ + standalone: true, + selector: '[jhiActiveMenu]', +}) +export default class ActiveMenuDirective implements OnInit { + jhiActiveMenu = input(); + + private el = inject(ElementRef); + private renderer = inject(Renderer2); + private translateService = inject(TranslateService); + + ngOnInit(): void { + this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { + this.updateActiveFlag(event.lang); + }); + + this.updateActiveFlag(this.translateService.currentLang); + } + + updateActiveFlag(selectedLanguage: string): void { + if (this.jhiActiveMenu() === selectedLanguage) { + this.renderer.addClass(this.el.nativeElement, 'active'); + } else { + this.renderer.removeClass(this.el.nativeElement, 'active'); + } + } +} diff --git a/src/main/webapp/app/layouts/navbar/navbar-item.model.d.ts b/src/main/webapp/app/layouts/navbar/navbar-item.model.d.ts new file mode 100644 index 0000000..d6f1cca --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar-item.model.d.ts @@ -0,0 +1,7 @@ +type NavbarItem = { + name: string; + route: string; + translationKey: string; +}; + +export default NavbarItem; diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.html b/src/main/webapp/app/layouts/navbar/navbar.component.html new file mode 100644 index 0000000..a753c32 --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.html @@ -0,0 +1,622 @@ + diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.scss b/src/main/webapp/app/layouts/navbar/navbar.component.scss new file mode 100644 index 0000000..b7b4014 --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.scss @@ -0,0 +1,50 @@ +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; + +/* ========================================================================== +Navbar +========================================================================== */ + +.navbar-version { + font-size: 0.65em; + color: $navbar-dark-color; +} + +.profile-image { + height: 1.75em; + width: 1.75em; +} + +.navbar { + padding: 0.2rem 1rem; + + a.nav-link { + font-weight: 400; + } +} + +/* ========================================================================== +Logo styles +========================================================================== */ +.logo-img { + height: 45px; + width: 200px; + display: inline-block; + vertical-align: middle; + background: url('/content/images/logo-nova-group-zero.svg') no-repeat center center; + background-size: contain; +} + +/* ========================================================================== +Fix navbar to top +========================================================================== */ +.navbar { + position: fixed; /* Fixes it at the top */ + top: 0; /* Aligns it to the top */ + left: 0; /* Ensures it starts from the left */ + width: 100%; /* Full-width */ + background-color: #333; /* Example background */ + color: white; + padding: 10px 20px; + z-index: 1000; /* Ensures it stays above other elements */ +} \ No newline at end of file diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.spec.ts b/src/main/webapp/app/layouts/navbar/navbar.component.spec.ts new file mode 100644 index 0000000..8243314 --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.spec.ts @@ -0,0 +1,95 @@ +jest.mock('app/login/login.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; + +import { ProfileInfo } from 'app/layouts/profiles/profile-info.model'; +import { Account } from 'app/core/auth/account.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { ProfileService } from 'app/layouts/profiles/profile.service'; +import { LoginService } from 'app/login/login.service'; + +import NavbarComponent from './navbar.component'; + +describe('Navbar Component', () => { + let comp: NavbarComponent; + let fixture: ComponentFixture; + let accountService: AccountService; + let profileService: ProfileService; + const account: Account = { + activated: true, + authorities: [], + email: '', + firstName: 'John', + langKey: '', + lastName: 'Doe', + login: 'john.doe', + imageUrl: '', + }; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NavbarComponent, HttpClientTestingModule, TranslateModule.forRoot()], + providers: [LoginService], + }) + .overrideTemplate(NavbarComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NavbarComponent); + comp = fixture.componentInstance; + accountService = TestBed.inject(AccountService); + profileService = TestBed.inject(ProfileService); + }); + + it('Should call profileService.getProfileInfo on init', () => { + // GIVEN + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(new ProfileInfo())); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(profileService.getProfileInfo).toHaveBeenCalled(); + }); + + it('Should hold current authenticated user in variable account', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account()).toBeNull(); + + // WHEN + accountService.authenticate(account); + + // THEN + expect(comp.account()).toEqual(account); + + // WHEN + accountService.authenticate(null); + + // THEN + expect(comp.account()).toBeNull(); + }); + + it('Should hold current authenticated user in variable account if user is authenticated before page load', () => { + // GIVEN + accountService.authenticate(account); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account()).toEqual(account); + + // WHEN + accountService.authenticate(null); + + // THEN + expect(comp.account()).toBeNull(); + }); +}); diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.ts b/src/main/webapp/app/layouts/navbar/navbar.component.ts new file mode 100644 index 0000000..79a82e9 --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.ts @@ -0,0 +1,154 @@ +import { Component, inject, signal, OnInit, OnDestroy, Renderer2 } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Subscription, map } from 'rxjs'; + +import { StateStorageService } from 'app/core/auth/state-storage.service'; +import SharedModule from 'app/shared/shared.module'; +import HasAnyAuthorityDirective from 'app/shared/auth/has-any-authority.directive'; +import { VERSION } from 'app/app.constants'; +import { LANGUAGES } from 'app/config/language.constants'; +import { AccountService } from 'app/core/auth/account.service'; +import { LoginService } from 'app/login/login.service'; +import { ProfileService } from 'app/layouts/profiles/profile.service'; +import { EntityNavbarItems } from 'app/entities/entity-navbar-items'; +import ActiveMenuDirective from './active-menu.directive'; +import NavbarItem from './navbar-item.model'; + +import {ResilientOrganizationsComponent} from 'app/resilient/resilient-environment/organizations/resilient-organizations.component' +import { DocumentService } from 'app/entities/document/service/document.service'; +import { IDocument } from 'app/entities/document/document.model'; +import { HttpResponse } from '@angular/common/http'; +import { DataUtils } from 'app/core/util/data-util.service'; +import { Resources } from 'app/security/resources.model'; +import { SecurityPermission } from 'app/security/security-permission.model'; +import { SecurityAction } from 'app/security/security-action.model'; + +@Component({ + standalone: true, + selector: 'jhi-navbar', + templateUrl: './navbar.component.html', + styleUrl: './navbar.component.scss', + imports: [RouterModule, SharedModule, HasAnyAuthorityDirective, ActiveMenuDirective, ResilientOrganizationsComponent], +}) +export default class NavbarComponent implements OnInit, OnDestroy { + private authSubscription?: Subscription; + + inProduction?: boolean; + isNavbarCollapsed = signal(true); + languages = LANGUAGES; + openAPIEnabled?: boolean; + version = ''; + entitiesNavbarItems: NavbarItem[] = []; + documents?: IDocument[]; + + private loginService = inject(LoginService); + private translateService = inject(TranslateService); + private stateStorageService = inject(StateStorageService); + private profileService = inject(ProfileService); + private router = inject(Router); + private accountService = inject(AccountService); + private documentService = inject(DocumentService); + protected dataUtils = inject(DataUtils); + + account = this.accountService.trackCurrentAccount(); + + /** + * This makes the enum accessible in the template + * Use it, mainly, in html templates for security. + * Example: + * + */ + Resources = Resources; + + constructor(private renderer: Renderer2) { + if (VERSION) { + this.version = VERSION.toLowerCase().startsWith('v') ? VERSION : `v${VERSION}`; + } + } + + ngOnInit(): void { + this.entitiesNavbarItems = EntityNavbarItems; + this.profileService.getProfileInfo().subscribe(profileInfo => { + this.inProduction = profileInfo.inProduction; + this.openAPIEnabled = profileInfo.openAPIEnabled; + }); + + this.authSubscription = this.accountService.getAuthenticationState().subscribe(account => { + if (account) { + this.renderer.addClass(document.body, 'authenticated'); + if (!this.documents || this.documents.length==0) { + // Only when user is authenticated. And if documents[] NOT already loaded. + // NOTE: Before, I was doing it always, for unauthenticated users it failed (security) and redirected him to login page. THIS is not intended, + // the user must be hable to access public anonymous HOME page. + this.loadDocuments(); + } + } else { + this.renderer.removeClass(document.body, 'authenticated'); + } + }); + + // add 'navbar-present' class + this.renderer.addClass(document.body, 'navbar-present'); + } + + ngOnDestroy(): void { + this.authSubscription?.unsubscribe(); + + // Remove class + this.renderer.removeClass(document.body, 'navbar-present'); + } + + changeLanguage(languageKey: string): void { + this.stateStorageService.storeLocale(languageKey); + this.translateService.use(languageKey); + } + + collapseNavbar(): void { + this.isNavbarCollapsed.set(true); + } + + login(): void { + this.router.navigate(['/login']); + } + + logout(): void { + this.collapseNavbar(); + this.loginService.logout(); + this.router.navigate(['']); + } + + toggleNavbar(): void { + this.isNavbarCollapsed.update(isNavbarCollapsed => !isNavbarCollapsed); + } + + hasRole(role: string): boolean { + return this.accountService.hasAnyAuthority([role]); + } + + hasReadPermission(evalResource: Resources): boolean { + const allowPermissions = [SecurityPermission.ALL, SecurityPermission.HIERARCHY]; + return allowPermissions.includes(this.accountService.getPermission(evalResource, SecurityAction.READ)); + } + + openFile(base64String: string, contentType: string | null | undefined): void { + this.dataUtils.openFile(base64String, contentType); + } + + protected loadDocuments(): void { + const queryObject: any = { + eagerload: true, + }; + + // Load documents + this.documentService + .query(queryObject) + .pipe( + map((res: HttpResponse) => res.body ?? []) + ).subscribe({ + next: (docs: IDocument[]) => { + this.documents = docs; + }, + });; + } +} diff --git a/src/main/webapp/app/layouts/profiles/page-ribbon.component.scss b/src/main/webapp/app/layouts/profiles/page-ribbon.component.scss new file mode 100644 index 0000000..fb15a18 --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/page-ribbon.component.scss @@ -0,0 +1,25 @@ +/* ========================================================================== +Development Ribbon +========================================================================== */ +.ribbon { + background-color: rgba(170, 0, 0, 0.5); + overflow: hidden; + position: absolute; + top: 40px; + white-space: nowrap; + width: 15em; + z-index: 9999; + pointer-events: none; + opacity: 0.75; + a { + color: #fff; + display: block; + font-weight: 400; + margin: 1px 0; + padding: 10px 50px; + text-align: center; + text-decoration: none; + text-shadow: 0 0 5px #444; + pointer-events: none; + } +} diff --git a/src/main/webapp/app/layouts/profiles/page-ribbon.component.spec.ts b/src/main/webapp/app/layouts/profiles/page-ribbon.component.spec.ts new file mode 100644 index 0000000..4d5176c --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/page-ribbon.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; + +import { ProfileInfo } from 'app/layouts/profiles/profile-info.model'; +import { ProfileService } from 'app/layouts/profiles/profile.service'; + +import PageRibbonComponent from './page-ribbon.component'; + +describe('Page Ribbon Component', () => { + let comp: PageRibbonComponent; + let fixture: ComponentFixture; + let profileService: ProfileService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PageRibbonComponent], + }) + .overrideTemplate(PageRibbonComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PageRibbonComponent); + comp = fixture.componentInstance; + profileService = TestBed.inject(ProfileService); + }); + + it('Should call profileService.getProfileInfo on init', () => { + // GIVEN + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(new ProfileInfo())); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(profileService.getProfileInfo).toHaveBeenCalled(); + }); +}); diff --git a/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts b/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts new file mode 100644 index 0000000..389cc38 --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts @@ -0,0 +1,29 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { ProfileService } from './profile.service'; + +@Component({ + standalone: true, + selector: 'jhi-page-ribbon', + template: ` + @if (ribbonEnv$ | async; as ribbonEnv) { + + } + `, + styleUrl: './page-ribbon.component.scss', + imports: [SharedModule], +}) +export default class PageRibbonComponent implements OnInit { + ribbonEnv$?: Observable; + + private profileService = inject(ProfileService); + + ngOnInit(): void { + this.ribbonEnv$ = this.profileService.getProfileInfo().pipe(map(profileInfo => profileInfo.ribbonEnv)); + } +} diff --git a/src/main/webapp/app/layouts/profiles/profile-info.model.ts b/src/main/webapp/app/layouts/profiles/profile-info.model.ts new file mode 100644 index 0000000..14e920f --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/profile-info.model.ts @@ -0,0 +1,15 @@ +export interface InfoResponse { + 'display-ribbon-on-profiles'?: string; + git?: any; + build?: any; + activeProfiles?: string[]; +} + +export class ProfileInfo { + constructor( + public activeProfiles?: string[], + public ribbonEnv?: string, + public inProduction?: boolean, + public openAPIEnabled?: boolean, + ) {} +} diff --git a/src/main/webapp/app/layouts/profiles/profile.service.ts b/src/main/webapp/app/layouts/profiles/profile.service.ts new file mode 100644 index 0000000..e6d61e3 --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/profile.service.ts @@ -0,0 +1,42 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { map, shareReplay } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ProfileInfo, InfoResponse } from './profile-info.model'; + +@Injectable({ providedIn: 'root' }) +export class ProfileService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + private infoUrl = this.applicationConfigService.getEndpointFor('management/info'); + private profileInfo$?: Observable; + + getProfileInfo(): Observable { + if (this.profileInfo$) { + return this.profileInfo$; + } + + this.profileInfo$ = this.http.get(this.infoUrl).pipe( + map((response: InfoResponse) => { + const profileInfo: ProfileInfo = { + activeProfiles: response.activeProfiles, + inProduction: response.activeProfiles?.includes('prod'), + openAPIEnabled: response.activeProfiles?.includes('api-docs'), + }; + if (response.activeProfiles && response['display-ribbon-on-profiles']) { + const displayRibbonOnProfiles = response['display-ribbon-on-profiles'].split(','); + const ribbonProfiles = displayRibbonOnProfiles.filter(profile => response.activeProfiles?.includes(profile)); + if (ribbonProfiles.length > 0) { + profileInfo.ribbonEnv = ribbonProfiles[0]; + } + } + return profileInfo; + }), + shareReplay(), + ); + return this.profileInfo$; + } +} diff --git a/src/main/webapp/app/login/login.component.css b/src/main/webapp/app/login/login.component.css new file mode 100644 index 0000000..2770099 --- /dev/null +++ b/src/main/webapp/app/login/login.component.css @@ -0,0 +1,90 @@ +html, body{ + height: 100%; + margin: 0; + font-family: 'Poppins', sans-serif; + color: #666; +} + + +h1,h2,h3,h4,h5,h6{ + color: #203f80; + font-weight: 900; + letter-spacing: -1px; +} +a{ + color: #348141; + font-weight: normal; + text-decoration: none; +} +a:hover{ + color: #348141; +} +label, +.col-form-label, +.control-label{ + padding-top: calc(0.375rem); + color: #999; + font-size: 0.9rem; +} +.form-control-lg{ + padding: 30px 25px; + border-radius: 15px; + background: #DDD; + font-size: 16px; + height: calc(1.5em + 1rem + 2px); + line-height: 1.5; +} +.form-group { + margin-bottom: 1rem; +} +.btn{ + border-radius: 15px; + font-weight: 700; + font-size: 14px; + line-height: 24px; +} +.btn-default{ + color: #333; + border: 2px solid #333; + background: #FFF; +} +.btn-default:hover{ + color: #348141; + border: 2px solid #348141; + background: #FFF; +} +.btn-primary{ + color: #FFF; + border: none; + background: #348141; +} +.btn-primary:hover{ + background: #5f905f; + color: #FFF; +} +.btn-lg{ + padding: 20px 25px; + font-size: 21px; +} +.bg-primary{ + background: #203f80 !important; + color: #FFF; +} +.btn-block { + display: block; + width: 100%; +} +.footer{ + height: 120px; + border-top: 1px #666 solid; + font-size: 13px; +} +.text-right { + text-align: right !important; +} + +.logo { + display: inline-block; + background: url('../../content/images/RouteZero.png') no-repeat left center; + background-size: contain; +} \ No newline at end of file diff --git a/src/main/webapp/app/login/login.component.html b/src/main/webapp/app/login/login.component.html new file mode 100644 index 0000000..d06f9b4 --- /dev/null +++ b/src/main/webapp/app/login/login.component.html @@ -0,0 +1,54 @@ +
+
+
+

+ + Route Zero +

+ + @if (authenticationError()) { +
+ Erro de autenticação! Por favor verifique as suas credenciais e tente novamente. +
+ } +
+
+ +
+
+ +
+

Esqueci-me da palavra passe

+ +
+

ou

+ +
+
+ + +
+
+
diff --git a/src/main/webapp/app/login/login.component.html.old b/src/main/webapp/app/login/login.component.html.old new file mode 100644 index 0000000..3d3b0d3 --- /dev/null +++ b/src/main/webapp/app/login/login.component.html.old @@ -0,0 +1,60 @@ +
+
+
+

Autenticação

+ @if (authenticationError()) { +
+ Erro de autenticação! Por favor verifique as suas credenciais e tente novamente. +
+ } +
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ + + +
+ Ainda não tem uma conta? + Crie uma nova conta +
+
+
+
diff --git a/src/main/webapp/app/login/login.component.spec.ts b/src/main/webapp/app/login/login.component.spec.ts new file mode 100644 index 0000000..2eaaa84 --- /dev/null +++ b/src/main/webapp/app/login/login.component.spec.ts @@ -0,0 +1,151 @@ +jest.mock('app/core/auth/account.service'); +jest.mock('app/login/login.service'); + +import { ElementRef, signal } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormBuilder } from '@angular/forms'; +import { Router, Navigation } from '@angular/router'; +import { of, throwError } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; + +import { LoginService } from './login.service'; +import LoginComponent from './login.component'; + +describe('LoginComponent', () => { + let comp: LoginComponent; + let fixture: ComponentFixture; + let mockRouter: Router; + let mockAccountService: AccountService; + let mockLoginService: LoginService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [LoginComponent], + providers: [ + FormBuilder, + AccountService, + { + provide: LoginService, + useValue: { + login: jest.fn(() => of({})), + }, + }, + ], + }) + .overrideTemplate(LoginComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + comp = fixture.componentInstance; + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockLoginService = TestBed.inject(LoginService); + mockAccountService = TestBed.inject(AccountService); + }); + + describe('ngOnInit', () => { + it('Should call accountService.identity on Init', () => { + // GIVEN + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockAccountService.identity).toHaveBeenCalled(); + }); + + it('Should call accountService.isAuthenticated on Init', () => { + // GIVEN + mockAccountService.identity = jest.fn(() => of(null)); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockAccountService.isAuthenticated).toHaveBeenCalled(); + }); + + it('should navigate to home page on Init if authenticated=true', () => { + // GIVEN + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + mockAccountService.isAuthenticated = () => true; + + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockRouter.navigate).toHaveBeenCalledWith(['']); + }); + }); + + describe('ngAfterViewInit', () => { + it('should set focus to username input after the view has been initialized', () => { + // GIVEN + const node = { + focus: jest.fn(), + }; + comp.username = signal(new ElementRef(node)); + + // WHEN + comp.ngAfterViewInit(); + + // THEN + expect(node.focus).toHaveBeenCalled(); + }); + }); + + describe('login', () => { + it('should authenticate the user and navigate to home page', () => { + // GIVEN + const credentials = { + username: 'admin', + password: 'admin', + rememberMe: true, + }; + + comp.loginForm.patchValue({ + username: 'admin', + password: 'admin', + rememberMe: true, + }); + + // WHEN + comp.login(); + + // THEN + expect(comp.authenticationError()).toEqual(false); + expect(mockLoginService.login).toHaveBeenCalledWith(credentials); + expect(mockRouter.navigate).toHaveBeenCalledWith(['']); + }); + + it('should authenticate the user but not navigate to home page if authentication process is already routing to cached url from localstorage', () => { + // GIVEN + jest.spyOn(mockRouter, 'getCurrentNavigation').mockReturnValue({} as Navigation); + + // WHEN + comp.login(); + + // THEN + expect(comp.authenticationError()).toEqual(false); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it('should stay on login form and show error message on login error', () => { + // GIVEN + mockLoginService.login = jest.fn(() => throwError(() => {})); + + // WHEN + comp.login(); + + // THEN + expect(comp.authenticationError()).toEqual(true); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/login/login.component.ts b/src/main/webapp/app/login/login.component.ts new file mode 100644 index 0000000..cf30f76 --- /dev/null +++ b/src/main/webapp/app/login/login.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit, AfterViewInit, ElementRef, inject, signal, viewChild } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Router, RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { LoginService } from 'app/login/login.service'; +import { AccountService } from 'app/core/auth/account.service'; + +@Component({ + standalone: true, + selector: 'jhi-login', + imports: [SharedModule, FormsModule, ReactiveFormsModule, RouterModule], + templateUrl: './login.component.html', + styleUrl: './login.component.css', +}) +export default class LoginComponent implements OnInit, AfterViewInit { + username = viewChild.required('username'); + saml2Endpoint : string | null = null; + authenticationError = signal(false); + + loginForm = new FormGroup({ + username: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + password: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + rememberMe: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), + }); + + private accountService = inject(AccountService); + private loginService = inject(LoginService); + private router = inject(Router); + + ngOnInit(): void { + // if already authenticated then navigate to home page + this.accountService.identity().subscribe(() => { + if (this.accountService.isAuthenticated()) { + this.router.navigate(['']); + } + }); + + this.accountService.saml2Endpoint().subscribe({ + next: (response: string | null) => { + this.saml2Endpoint = response; + }, + error: (err) => { + console.error('Failed to fetch SAML2 endpoint', err); + } + }); + } + + ngAfterViewInit(): void { + this.username().nativeElement.focus(); + } + + login(): void { + this.loginService.login(this.loginForm.getRawValue()).subscribe({ + next: () => { + this.authenticationError.set(false); + if (!this.router.getCurrentNavigation()) { + // There were no routing during login (eg from navigationToStoredUrl) + this.router.navigate(['']); + } + }, + error: () => this.authenticationError.set(true), + }); + } + + samlLogin() { + if (this.saml2Endpoint) { + window.location.href = this.loginService.samlLoginUrl(this.saml2Endpoint); // Redirect to SAML login + } + } +} diff --git a/src/main/webapp/app/login/login.model.ts b/src/main/webapp/app/login/login.model.ts new file mode 100644 index 0000000..10faab7 --- /dev/null +++ b/src/main/webapp/app/login/login.model.ts @@ -0,0 +1,7 @@ +export class Login { + constructor( + public username: string, + public password: string, + public rememberMe: boolean, + ) {} +} diff --git a/src/main/webapp/app/login/login.service.ts b/src/main/webapp/app/login/login.service.ts new file mode 100644 index 0000000..dbeb28c --- /dev/null +++ b/src/main/webapp/app/login/login.service.ts @@ -0,0 +1,38 @@ +import { inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { Account } from 'app/core/auth/account.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { AuthServerProvider } from 'app/core/auth/auth-session.service'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { Login } from './login.model'; + +@Injectable({ providedIn: 'root' }) +export class LoginService { + private applicationConfigService = inject(ApplicationConfigService); + private accountService = inject(AccountService); + private authServerProvider = inject(AuthServerProvider); + + login(credentials: Login): Observable { + return this.authServerProvider.login(credentials).pipe(mergeMap(() => this.accountService.identity(true))); + } + + samlLoginUrl(endpoint: string): string { + // See the file /resilient/webpack/proxy.conf.js, this bypasses the url /saml2/* directly to server + // needed, when running front-end and back-end seperatly. + return this.applicationConfigService.getEndpointFor(`/saml2/authenticate/${endpoint}`); + } + + logoutUrl(): string { + return this.applicationConfigService.getEndpointFor('api/logout'); + } + + logoutInClient(): void { + this.accountService.authenticate(null); + } + + logout(): void { + this.authServerProvider.logout().subscribe({ complete: () => this.accountService.authenticate(null) }); + } +} diff --git a/src/main/webapp/app/resilient/codemirrorfield/codemirrorfield.component - Copy.ts b/src/main/webapp/app/resilient/codemirrorfield/codemirrorfield.component - Copy.ts new file mode 100644 index 0000000..a2f5a97 --- /dev/null +++ b/src/main/webapp/app/resilient/codemirrorfield/codemirrorfield.component - Copy.ts @@ -0,0 +1,90 @@ +import { + Component, + ElementRef, + Input, + OnInit, + ViewChild, + OnDestroy, +} from '@angular/core'; +import { EditorView, basicSetup } from 'codemirror'; +import { FormControl } from '@angular/forms'; +import { EditorState } from '@codemirror/state'; +import { java } from '@codemirror/lang-java'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +@Component({ + standalone: true, + selector: 'code-mirror-field', + template: `
`, +}) +export class CodeMirrorFieldComponent implements OnInit, OnDestroy { + @ViewChild('editorContainer', { static: true }) editorContainer!: ElementRef; + @Input() formControl!: FormControl; // FormControl vindo do formulário reativo + + private editor!: EditorView; + + ngOnInit(): void { + this.initializeEditor(); + + // Atualizar o editor quando o valor do FormControl muda + this.formControl.valueChanges.subscribe((value) => { + if (this.editor.state.doc.toString() !== value) { + this.updateEditorContent(value); + } + }); + } + + ngOnDestroy(): void { + // Destruir o editor ao sair do componente + this.editor.destroy(); + } + + private initializeEditor(): void { + const cursorListener = EditorView.updateListener.of((update) => { + if (update.selectionSet) { + const cursorPosition = update.state.selection.main.head; + const word = this.getCurrentWord(update.state, cursorPosition); + console.log('Current word:', word); + } + }); + + const updateListener = EditorView.updateListener.of((update) => { + if (update.docChanged) { + const content = update.state.doc.toString(); + this.formControl.setValue(content, { emitEvent: false }); // Atualizar FormControl + } + }); + + this.editor = new EditorView({ + state: EditorState.create({ + doc: this.formControl.value || '', // Inicializa com o valor do FormControl + extensions: [basicSetup, java(), updateListener, cursorListener], + }), + parent: this.editorContainer.nativeElement, + }); + } + + private updateEditorContent(value: string): void { + this.editor.dispatch({ + changes: { from: 0, to: this.editor.state.doc.length, insert: value }, + }); + } + + /** + * Get the current word based on cursor position + */ + private getCurrentWord(state: EditorState, position: number): string | null { + const line = state.doc.lineAt(position); // Get the current line + const lineText = line.text; + const cursorIndex = position - line.from; // Cursor position within the line + + // Find the word around the cursor + const left = lineText.slice(0, cursorIndex).match(/\b\w+$/); + const right = lineText.slice(cursorIndex).match(/^\w+\b/); + + const leftWord = left ? left[0] : ''; + const rightWord = right ? right[0] : ''; + + return leftWord + rightWord; // Combine both parts to get the full word + } +} diff --git a/src/main/webapp/app/resilient/codemirrorfield/codemirrorfield.component.ts b/src/main/webapp/app/resilient/codemirrorfield/codemirrorfield.component.ts new file mode 100644 index 0000000..fdd7949 --- /dev/null +++ b/src/main/webapp/app/resilient/codemirrorfield/codemirrorfield.component.ts @@ -0,0 +1,145 @@ +import { + Component, + ElementRef, + forwardRef, + Input, + OnInit, + ViewChild, + OnDestroy, +} from '@angular/core'; +import { EditorView, basicSetup } from 'codemirror'; +import { FormControl } from '@angular/forms'; +import { EditorState } from '@codemirror/state'; +import { java } from '@codemirror/lang-java'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + standalone: true, + selector: 'code-mirror-field', + template: `
`, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CodeMirrorFieldComponent), + multi: true + } + ] +}) +export class CodeMirrorFieldComponent implements OnInit, OnDestroy, ControlValueAccessor { + @ViewChild('editorContainer', { static: true }) editorContainer!: ElementRef; + + private editor!: EditorView; + private readonly: boolean = false; + + value: any = ''; + onChange: (value: any) => void = () => {}; + onTouched: () => void = () => {}; + + private cursorListener = EditorView.updateListener.of((update) => { + if (update.selectionSet) { + const cursorPosition = update.state.selection.main.head; + const word = this.getCurrentWord(update.state, cursorPosition); + console.log('Current word:', word); + } + }); + + private updateListener = EditorView.updateListener.of((update) => { + if (update.docChanged) { + const content = update.state.doc.toString(); + this.value = content; + this.onChange(this.value); // Fire ControlValueAccessor change event + this.onTouched(); // Fire ControlValueAccessor touched event + } + }); + + /* Interface OnInit */ + ngOnInit(): void { + this.initializeEditor(); + } + + /* Interface OnDestroy */ + ngOnDestroy(): void { + // Destruir o editor ao sair do componente + this.editor.destroy(); + } + + /* Interface ControlValueAccessor */ + writeValue(value: any): void { + this.value = value; + this.updateEditorContent(value); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.readonly = isDisabled; + this.editor.setState(this.createEditorState()); + } + + private initializeEditor(): void { + /* + this.editor = new EditorView({ + state: EditorState.create({ + doc: '', // Initialize empty. The ControlValueAccessor.writeValue will be called and set the value + extensions: [basicSetup, java(), this.updateListener, this.cursorListener, EditorView.editable.of(this.readonly)], + }), + parent: this.editorContainer.nativeElement, + }); + */ + this.editor = new EditorView({ + state: this.createEditorState(), + parent: this.editorContainer.nativeElement, + }); + } + + private createEditorState(): EditorState{ + let content = ''; + if (this.editor) { + content = this.editor.state.doc.toString(); // Preserve the current document + } + + const newState = EditorState.create({ + doc: content, + extensions: [ + basicSetup, + java(), + this.updateListener, + this.cursorListener, + EditorView.editable.of(!this.readonly), // Set readonly mode + ], + }); + + return newState; + } + + private updateEditorContent(value: string): void { + this.editor.dispatch({ + changes: { from: 0, to: this.editor.state.doc.length, insert: value }, + }); + } + + /** + * Get the current word based on cursor position + */ + private getCurrentWord(state: EditorState, position: number): string | null { + const line = state.doc.lineAt(position); // Get the current line + const lineText = line.text; + const cursorIndex = position - line.from; // Cursor position within the line + + // Find the word around the cursor + const left = lineText.slice(0, cursorIndex).match(/\b\w+$/); + const right = lineText.slice(cursorIndex).match(/^\w+\b/); + + const leftWord = left ? left[0] : ''; + const rightWord = right ? right[0] : ''; + + return leftWord + rightWord; // Combine both parts to get the full word + } +} diff --git a/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.html b/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.html new file mode 100644 index 0000000..101f23b --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.html @@ -0,0 +1,26 @@ + +
+ + + + + +
diff --git a/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.spec.ts b/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.spec.ts new file mode 100644 index 0000000..a6eed6d --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.spec.ts @@ -0,0 +1,62 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { ResilientActivityService } from '../../service/resilient-activity.service'; + +import { ResilientActivityActionsButtonDialogComponent } from './resilient-activity-actions-button-dialog.component'; + +describe('Resilient Activity Actions Button Dialog Component', () => { + let comp: ResilientActivityActionsButtonDialogComponent; + let fixture: ComponentFixture; + let service: InputDataUploadService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, ResilientActivityActionsButtonDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(ResilientActivityActionsButtonDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(ResilientActivityActionsButtonDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(ResilientActivityService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + describe('confirmActivityInvoke', () => { + it('Should call activity engine service on confirmActivityInvoke', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'import').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmImport(123); + tick(); + + // THEN + expect(service.import).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('imported'); + }), + )); + + it('Should not call import service on clear', () => { + // GIVEN + jest.spyOn(service, 'import'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.import).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.ts b/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.ts new file mode 100644 index 0000000..fd807f1 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/actions-button/dialog/resilient-activity-actions-button-dialog.component.ts @@ -0,0 +1,42 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateService } from '@ngx-translate/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +import SharedModule from 'app/shared/shared.module'; + +export const CONFIRM_EVENT = 'CONFIRM_EVENT'; + +@Component({ + standalone: true, + templateUrl: './resilient-activity-actions-button-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class ResilientActivityActionsButtonDialogComponent implements OnInit { + activityKey?: string; + + safeBody?: SafeHtml; + safeTitle?: SafeHtml; + + protected activeModal = inject(NgbActiveModal); + protected translate = inject(TranslateService); + protected sanitizer = inject(DomSanitizer); + + ngOnInit(): void { + this.translate.get(`resilientApp.activities.${this.activityKey}.dialog.body`).subscribe((translation: string) => { + this.safeBody = this.sanitizer.bypassSecurityTrustHtml(translation); + }); + this.translate.get(`resilientApp.activities.${this.activityKey}.dialog.title`).subscribe((translation: string) => { + this.safeTitle = this.sanitizer.bypassSecurityTrustHtml(translation); + }); + } + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmActivityAction(): void { + this.activeModal.close(CONFIRM_EVENT); + } +} diff --git a/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.html b/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.html new file mode 100644 index 0000000..46ef28c --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.html @@ -0,0 +1,11 @@ + +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.spec.ts b/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.spec.ts new file mode 100644 index 0000000..e4bbb29 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { ResilientActivityActionsButtonComponent } from './resilient-activity-actions-button.component'; + +describe('Resilient Activity Actions Button Component', () => { + let comp: ResilientActivityActionsButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResilientActivityActionsButtonComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: ResilientActivityActionsButtonComponent, + resolve: { period: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(ResilientActivityActionsButtonComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ResilientActivityActionsButtonComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load period on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', ResilientActivityActionsButtonComponent); + + // THEN + expect(instance.period()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.ts b/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.ts new file mode 100644 index 0000000..a2b677c --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component.ts @@ -0,0 +1,108 @@ +/** + * ResilientActivityActionsButton WIDGET + * + * Resilient component widget to show dropdown buttons for each activity allowed in the current ActivityDomain's. Allowing for activity execution. + * + * Input's: + * - activityDomain: The entity domain instance. Must be a ActivityDomain + * + * Event's: + * - activityInvoked: Fired when one of the buttons is pressed, that in turn fires server-side activity execution. The event sends a string, with the activity key of the fired activity + * + * Usage + * - In the intended *.component.html insert the code: + * + * - In the *.component.ts insert the code: + * import { ResilientActivityActionsButtonComponent } from 'app/resilient/resilient-activity/actions-button/resilient-activity-actions-button.component'; + * ... + * _entity = input(null); + * ... + * _activityInvokedHandler(message: string): void { + * + * } + */ + +import { Component, Output, Input, inject, OnInit, EventEmitter } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; +import { map, tap, filter } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { IActivityInfo, IActivityInvoke, IActivityInvokeResponse } from '../resilient-activity.model'; +import { ResilientActivityService } from '../service/resilient-activity.service'; +import { ResilientActivityActionsButtonDialogComponent, CONFIRM_EVENT } from './dialog/resilient-activity-actions-button-dialog.component'; +import { IActivityDomain } from 'app/entities/activity/activity.model'; + +@Component({ + standalone: true, + selector: 'resilient-activity-actions-button', + templateUrl: './resilient-activity-actions-button.component.html', + imports: [SharedModule, RouterModule, ResilientActivityActionsButtonDialogComponent] +}) +export class ResilientActivityActionsButtonComponent implements OnInit { + @Output() activityInvoked = new EventEmitter(); + + @Input() activityDomain: IActivityDomain | null = null; // Assumes an Entity instance that extends ActivityDomain + + activityCollection: IActivityInfo[] = []; + + protected resilientActivityService = inject(ResilientActivityService); + protected modalService = inject(NgbModal); + + ngOnInit(): void { + if (this.activityDomain && !this.activityDomain.activityProgressKey) { + this.loadActivitiesForActivityDomain(); + } + } + + previousState(): void { + window.history.back(); + } + + doActivity(activityKey: string): void { + const modalRef = this.modalService.open(ResilientActivityActionsButtonDialogComponent, { size: 'lg', backdrop: 'static' }); + // Set modal dialog properties + modalRef.componentInstance.activityKey = activityKey; + + modalRef.closed + .pipe( + filter(reason => reason === CONFIRM_EVENT), + tap(() => this.invokeActivity(activityKey)), + ) + .subscribe(); + } + + protected invokeActivity(activityKey: string): void { + // Can't invoke activity without a domain id + if (!this.activityDomain?.id) return; + // Can't invoke activity without a domain class + if (!this.activityDomain?.activityDomainClass) return; + + const activityInvoke: IActivityInvoke = { + activityKey: activityKey, + activityDomainClass: this.activityDomain?.activityDomainClass, + activityDomainId: this.activityDomain?.id, + }; + + this.resilientActivityService + .doActivity(activityInvoke) + .subscribe((activityInvokeResponse: IActivityInvokeResponse | null) => { + this.activityInvoked.emit(activityInvokeResponse?.activityKey); // Emit the event with the message + }); + } + + protected loadActivitiesForActivityDomain(): void { + // Its a ActivityDomain? + if (this.activityDomain?.activityDomainClass) { + // Get ActivityDomain.state + const activityDomainState = this.activityDomain?.state; + + this.resilientActivityService + .activityInfo(this.activityDomain?.activityDomainClass, activityDomainState) + .pipe(map((res: HttpResponse) => res.body ?? [])) + .subscribe((activities: IActivityInfo[]) => (this.activityCollection = activities)); + } + } + +} diff --git a/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.html b/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.html new file mode 100644 index 0000000..7be71e5 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.html @@ -0,0 +1,8 @@ +
+ + +
+
{{ progress }}%
+
{{ progressInfo }}
+
+
\ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.spec.ts b/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.spec.ts new file mode 100644 index 0000000..f6f3a13 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { ResilientActivityProgressComponent } from './resilient-activity-progress.component'; + +describe('Resilient Activity Progress Component', () => { + let comp: ResilientActivityProgressComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResilientActivityProgressComponent], + providers: [ + provideRouter( + [ + { + path: '**', + component: ResilientActivityProgressComponent, + resolve: { period: () => of({ id: 123 }) }, + }, + ], + withComponentInputBinding(), + ), + ], + }) + .overrideTemplate(ResilientActivityProgressComponent, '') + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ResilientActivityProgressComponent); + comp = fixture.componentInstance; + }); + + describe('OnInit', () => { + it('Should load period on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', ResilientActivityProgressComponent); + + // THEN + expect(instance.period()).toEqual(expect.objectContaining({ id: 123 })); + }); + }); + + describe('PreviousState', () => { + it('Should navigate to previous state', () => { + jest.spyOn(window.history, 'back'); + comp.previousState(); + expect(window.history.back).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.ts b/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.ts new file mode 100644 index 0000000..5408ffe --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/progress/resilient-activity-progress.component.ts @@ -0,0 +1,119 @@ +/** + * ResilientActivityProgress WIDGET + * + * Resilient component widget to show a progress bar, for ActivityDomain's. + * Input's: + * - activityDomain: The entity domain instance. Must be a ActivityDomain + * + * Event's: + * - activityFinished: Fired when the progress bar reaches the end. The event sends a string, with the constant value "ended" + * + * Usage + * - In the intended *.component.html insert the code: + * + * - In the *.component.ts insert the code: + * import { ResilientActivityProgressComponent } from 'app/resilient/resilient-activity/progress/resilient-activity-progress.component'; + * ... + * _entity = input(null); + * ... + * _activityFinishedHandler(message: string): void { + * + * } + */ + +import { Component, Input, Output, OnInit, EventEmitter, inject } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpResponse } from '@angular/common/http'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { IActivityProgress } from '../resilient-activity.model'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; + +import { ResilientActivityService } from '../service/resilient-activity.service'; + +@Component({ + standalone: true, + selector: 'jhi-activity-progress', + templateUrl: './resilient-activity-progress.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe, MatProgressBarModule], + styles: [` + mat-progress-bar { margin: 10px 0; width: 100%; } + `] +}) +export class ResilientActivityProgressComponent implements OnInit { + @Output() activityFinished = new EventEmitter(); + @Input() activityDomain : any; // Assumes an Entity instance that extends ActivityDomain + + progress: number = -1; + progressInfo: String = ""; + + protected resilientActivityService = inject(ResilientActivityService); + + ngOnInit(): void { + this.startProgress(); + } + + previousState(): void { + window.history.back(); + } + + startProgress(): void { + // Can't check progress activity without a domain id + if (!this.activityDomain?.id) return; + // Can't check progress activity without a domain class + if (!this.activityDomain?.activityDomainClass) return; + + const activityKey = this.activityDomain?.activityDomainClass + "@" + this.activityDomain?.id; + + // Default. Just to show the progress bar + // this.progress = -1; + // this.progressInfo = "In progress ..."; + + // Invoke remote activity progress service to get the progress + const intervalId = setInterval(() => { + this.resilientActivityService + .progress(activityKey) + .subscribe({ + next: (res: HttpResponse) => { + const activityProgress: IActivityProgress | null = res.body ?? null; + if (activityProgress) { + const oldProgress = this.progress; + + this.progress = activityProgress.progressPercentage; + this.progressInfo = activityProgress.progressTask; + + if (this.progress < 0 || (oldProgress < 0 && this.progress >= 100)) { + // this.progress < 0 => Negative progress means: nothing is running in the server + // (oldProgress < 0 && this.progress >= 100) => Progress is 100%, but the progress bar was never started (-1). This is an old progress, that alresdy finished. + clearInterval(intervalId); // Stop the loop + this.progress = -1; + } else if (this.progress >= 100) { + clearInterval(intervalId); // Stop the loop + this.progress = 100; // No sense in having a job with >100% + this.activityFinished.emit("ended"); // Fire event for other component's reaction + } else if (oldProgress > 0 && this.progress < 0) { + // Special case. It was in progress and ended. + // When it ends, the progress is removed from the server. + // Thats why a negative progress was returned. + // Assume same behavior as 100%. + clearInterval(intervalId); + this.progress = 100; + this.activityFinished.emit("ended"); + } + } else { + //This is strange. Just report an error in the progressInfo + this.progressInfo = "Unknown progress information"; + } + }, + error: (res: HttpResponse) => { + // Error. No sense in keeping invoking the server. + // Give up + this.progressInfo = "Error fetching the progress information. STOPED"; + clearInterval(intervalId); + }, + }) + ; + }, 1000); + } +} diff --git a/src/main/webapp/app/resilient/resilient-activity/resilient-activity.model.ts b/src/main/webapp/app/resilient/resilient-activity/resilient-activity.model.ts new file mode 100644 index 0000000..1259237 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/resilient-activity.model.ts @@ -0,0 +1,24 @@ +// All Entities that are Activity enabled, must extend this interface. +export interface IActivityDomain { + id: number; + activityDomainClass: string; +} + +export interface IActivityProgress { + progressPercentage: number; + progressTask: string; +} + +export interface IActivityInfo { + activityKey: string; +} + +export interface IActivityInvoke { + activityKey: string; + activityDomainClass: string; + activityDomainId: number; +} + +export interface IActivityInvokeResponse { + activityKey: string; +} \ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-activity/service/resilient-activity.service.spec.ts b/src/main/webapp/app/resilient/resilient-activity/service/resilient-activity.service.spec.ts new file mode 100644 index 0000000..254fdc6 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/service/resilient-activity.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ResilientActivityService } from './resilient-activity.service'; + +describe('ResilientActivityService', () => { + let service: ResilientActivityService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ResilientActivityService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/main/webapp/app/resilient/resilient-activity/service/resilient-activity.service.ts b/src/main/webapp/app/resilient/resilient-activity/service/resilient-activity.service.ts new file mode 100644 index 0000000..3073dcd --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-activity/service/resilient-activity.service.ts @@ -0,0 +1,37 @@ +import { inject, Injectable } from '@angular/core'; +import { Observable, BehaviorSubject, map } from 'rxjs'; +import { IActivityProgress, IActivityInfo, IActivityInvoke, IActivityInvokeResponse } from '../resilient-activity.model'; +import { createRequestOption } from 'app/core/request/request-util'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { HttpClient, HttpResponse } from '@angular/common/http'; + +export type EntityResponseType = HttpResponse; + +export type EntityArrayResponseType = HttpResponse; + +export type ActivityInvokeResponseType = HttpResponse; + +@Injectable({ + providedIn: 'root' +}) +export class ResilientActivityService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/activity-engines'); + + progress(key: string): Observable { + return this.http + .get(`${this.resourceUrl}/progress/${key}`, { observe: 'response' }); + } + + activityInfo(activityDomainClass: string, activityDomainState?: string): Observable { + return this.http + .get(`${this.resourceUrl}/activity/${activityDomainClass}/${activityDomainState}`, { observe: 'response' }); + } + + doActivity(activityInvoke: IActivityInvoke): Observable { + return this.http + .put(`${this.resourceUrl}/activity`, activityInvoke, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body)); + } +} diff --git a/src/main/webapp/app/resilient/resilient-base/resilient-base.component.ts b/src/main/webapp/app/resilient/resilient-base/resilient-base.component.ts new file mode 100644 index 0000000..52e5f2d --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-base/resilient-base.component.ts @@ -0,0 +1,41 @@ +import { inject } from '@angular/core'; +import { AccountService } from 'app/core/auth/account.service'; +import { Resources } from 'app/security/resources.model'; +import { SecurityAction } from 'app/security/security-action.model'; + +export abstract class ResilientBaseComponent { + protected accountService = inject(AccountService); + + Resources = Resources; // ✅ This makes the enum accessible in the template + + /* + * Security Permission methods. Evaluates if the current authenticated user has PERMISSION to do the action + * In html template, if security permission fails, the secured element is hidden. + */ + hasDeletePermission(evalResource: string): boolean { + return this.accountService.evalPermission(evalResource, SecurityAction.DELETE); + } + + hasUpdatePermission(evalResource: string): boolean { + return this.accountService.evalPermission(evalResource, SecurityAction.UPDATE); + } + + hasCreatePermission(evalResource: string): boolean { + return this.accountService.evalPermission(evalResource, SecurityAction.CREATE); + } + + hasReadPermission(evalResource: string): boolean { + return this.accountService.evalPermission(evalResource, SecurityAction.READ); + } + + /* + * Action Permission methods. Evaluates if the current authenticated user AND/OR + * the current Entity instance allows the action to be done. Example, the User hasPermission to DELETE, + * but the Entity instance is in a state that DOESN'T allow it. + * In html template, if action permission fails, the element is showned but disabled + */ + canCreate(): boolean { return true; } + canRead(): boolean { return true; } + canUpdate(): boolean { return true; } + canDelete(): boolean { return true; } +} \ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-base/resilient-base.model.ts b/src/main/webapp/app/resilient/resilient-base/resilient-base.model.ts new file mode 100644 index 0000000..c5f25f3 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-base/resilient-base.model.ts @@ -0,0 +1,4 @@ +export interface IResilientEntity { + id: number; + version?: number | null; +} \ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-base/resilient-base.service.ts b/src/main/webapp/app/resilient/resilient-base/resilient-base.service.ts new file mode 100644 index 0000000..2630e02 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-base/resilient-base.service.ts @@ -0,0 +1,329 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable, map } from 'rxjs'; +import { DATE_FORMAT } from 'app/config/input.constants'; +import dayjs from 'dayjs/esm'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { createRequestOption } from 'app/core/request/request-util'; + +import { IResilientEntity } from './resilient-base.model'; + +export type PartialUpdateEntity = Partial & Pick; +export type EntityResponseType = HttpResponse +export type EntityArrayResponseType = HttpResponse; + +/** + * Needed type to change DATE properties into STRING properties. + * How it works: + * #1. Loops through each key (K) in T. + * #2. If T[K] is of type Date, replace it with string | null. + * #3. Otherwise, keep the original type. + */ +export type RestOf = { + [K in keyof T]: T[K] extends Date ? string | null : T[K]; +}; + +export enum DateTypeEnum { + DATE = "DATE", /* Date only : yyyy-MM-dd */ + TIMESTAMP = "TIMESTAMP" /* Date and Time : yyyy-MM-ddTHH:mm:ss.s */ +} + +/** + * Base Resiliente Service, with common methods: + *
    + *
  • create - REST POST call to endpoint: /""
  • + *
  • UPDATE - REST PUT call to endpoint: /{id}
  • + *
+ * @param Entity domain interface (Ex. Period) + * @param Entity domain new interface (Ex. NewPeriod) + * @param Entity domain partial interface (Ex. PartialUpdatePeriod) + * @class + */ +@Injectable({ providedIn: 'root' }) +export abstract class ResilientService { + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + + /** + * Overridable method to provide the resource main URI. Something like : 'api/entities'. + * Implementation sample: + *
+   * private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/entities');
+   * override getResourceUrl(): string { return this.resourceUrl; }
+   * 
+ */ + protected abstract getResourceUrl(): string; + + /** + * Overridable method to provide a list of DATE properties in entity type. + * Will be used to auto-convert DATE properties to and from JSON string. + * + *
+   * private readonly dateProperties: Map = new Map([
+   *   ["propertyDate", DateTypeEnum.DATE],
+   *   ["propertyTimestamp", DateTypeEnum.TIMESTAMP]
+   * ]);
+   * override getDateProperties(): Map { return this.dateProperties; }
+   * 
+ */ + protected getDateProperties(): Map { + return new Map(); + } + + private needsDataConvertion(): Boolean { + return this.getDateProperties().size>0; + } + + create(newEntity: NE): Observable> { + if (this.needsDataConvertion()) { + // Convert date properties + const convertedEntity = this.convertDateFromClient(newEntity); + return this.http + .post>(this.getResourceUrl(), convertedEntity, { observe: 'response' }) + .pipe(map(res => this.convertResponseFromServer(res))); + } else { + // No convertion is needed + return this.http.post(this.getResourceUrl(), newEntity, { observe: 'response' }); + } + } + + update(entity: IE): Observable> { + if (this.needsDataConvertion()) { + // Convert date properties + const copy = this.convertDateFromClient(entity); + return this.http + .put>(`${this.getResourceUrl()}/${this.getEntityIdentifier(entity)}`, copy, { observe: 'response' }) + .pipe(map(res => this.convertResponseFromServer(res))); + } else { + // No convertion is needed + return this.http.put(`${this.getResourceUrl()}/${this.getEntityIdentifier(entity)}`, entity, { + observe: 'response', + }); + } + } + + find(id: number): Observable> { + if (this.needsDataConvertion()) { + // Convert date properties + return this.http + .get>(`${this.getResourceUrl()}/${id}`, { observe: 'response' }) + .pipe(map(res => this.convertResponseFromServer(res))); + } else { + // No convertion is needed + return this.http.get(`${this.getResourceUrl()}/${id}`, { observe: 'response' }); + } + } + + query(req?: any): Observable> { + const options = createRequestOption(req); + + if (this.needsDataConvertion()) { + // Convert date properties + return this.http + .get[]>(this.getResourceUrl(), { params: options, observe: 'response' }) + .pipe(map(res => this.convertResponseArrayFromServer(res))); + } else { + // No convertion is needed + return this.http.get(this.getResourceUrl(), { params: options, observe: 'response' }); + } + } + + queryByCriteria(req?: any): Observable> { + const options = createRequestOption(req); + + if (this.needsDataConvertion()) { + // Convert date properties + return this.http + .get[]>(`${this.getResourceUrl()}/criteria`, { params: options, observe: 'response' }) + .pipe(map(res => this.convertResponseArrayFromServer(res))); + } else { + // No convertion is needed + return this.http.get(`${this.getResourceUrl()}/criteria`, { params: options, observe: 'response' }); + } + } + + delete(id: number): Observable> { + return this.http.delete(`${this.getResourceUrl()}/${id}`, { observe: 'response' }); + } + + partialUpdate(entity: PartialUpdateEntity): Observable> { + return this.http.patch(`${this.getResourceUrl()}/${this.getEntityIdentifier(entity)}`, entity, { + observe: 'response', + }); + } + + getEntityIdentifier(entity: Pick): number { + return entity.id; + } + + compareEntity(o1: Pick | null, o2: Pick | null): boolean { + return o1 && o2 ? this.getEntityIdentifier(o1) === this.getEntityIdentifier(o2) : o1 === o2; + } + + addEntityToCollectionIfMissing>( + entityCollection: Type[], + ...entitiesToCheck: (Type | null | undefined)[] + ): Type[] { + const entities: Type[] = entitiesToCheck.filter(isPresent); + if (entities.length > 0) { + const entityCollectionIdentifiers = entityCollection.map(entityItem => + this.getEntityIdentifier(entityItem), + ); + const entitiesToAdd = entities.filter(entityItem => { + const entityIdentifier = this.getEntityIdentifier(entityItem); + if (entityCollectionIdentifiers.includes(entityIdentifier)) { + return false; + } + entityCollectionIdentifiers.push(entityIdentifier); + return true; + }); + return [...entitiesToAdd, ...entityCollection]; + } + return entityCollection; + } + + /* DATE Convertion methods */ + + /* + * ORIGINAL method code generated by JHipster, for future reference : + protected convertDateFromClient(entity: T): RestOf { + const result: any = { + ...entity, + propertyDate: entity.propertyDate?.format(DATE_FORMAT) ?? null, + propertyTimestamp: entity.propertyTimestamp?.toJSON() ?? null, + }; + + return result; + } + */ + protected convertDateFromClient>(entity: T): RestOf { + const result: Partial = { ...entity }; + + for (const key in entity) { + //Search the entity property in the date collection + const type = this.getDateProperties().get(key); + + //If found, convert (Object into String) + if (type !== undefined) { + //Get the value from entity instance + const value = entity[key]; + + if (!value) { + (result as any)[key] = null; + continue; + } + + // Must be of type Dayjs (JHipster always defines date properties as Dayjs) + if (!(dayjs.isDayjs(value))) { + continue; + } + + // Check if it's a Date or a DayJS type + if (type === DateTypeEnum.TIMESTAMP) { + (result as any)[key] = value.toJSON(); + } else if (type === DateTypeEnum.DATE) { + (result as any)[key] = value.format(DATE_FORMAT); + } else { + // ERROR. This is invalid option + console.error("❌ ERROR: Invalid DateProperty type. Valid types are: DATE | TIMESTAMP. But the type is '" + type + "'"); + throw new Error("❌ ERROR: Invalid DateProperty type. Valid types are: DATE | TIMESTAMP. But the type is '" + type + "'"); + } + } + } + + return result as RestOf;; + } + + /* + * ORIGINAL method code generated by JHipster, for future reference : + protected convertDateFromServer(restEntity: RestEntity): IEntity { + const result: any = { + ...restEntity, + propertyDate: restEntity.propertyDate ? dayjs(restEntity.propertyDate) : undefined, + propertyTimestamp: restEntity.propertyTimestamp ? dayjs(restEntity.propertyTimestamp) : undefined, + }; + + return result; + } + */ + + protected convertDateFromServer(restEntity: RestOf): IE { + const result: Partial> = { ...restEntity }; + + for (const key in restEntity) { + //Search the entity property in the date collection + const type = this.getDateProperties().get(key); + + //If found, convert (String into Object) + if (type !== undefined) { + //Get the value from restEntity instance + const value = restEntity[key as keyof RestOf]; + + if (!value) { + (result as any)[key] = undefined; + continue; + } + + // Create a Dayjs instance. No matter if its DATE or TIMESTAMP + if (value && (typeof value === "string" || typeof value === "number" || value instanceof Date)) { + (result as any)[key] = dayjs(value); + } + } + } + + return result as IE; + } + + protected convertResponseFromServer(res: HttpResponse>): HttpResponse { + return res.clone({ + body: res.body ? this.convertDateFromServer(res.body) : null, + }); + } + + protected convertResponseArrayFromServer(res: HttpResponse[]>): HttpResponse { + return res.clone({ + body: res.body ? res.body.map(item => this.convertDateFromServer(item)) : null, + }); + } + + /* Action Permission methods */ + /** + * Method that evaluates if the @Input() {entity} can be created. This must answer the question: + * - The entity state (or any other business rule) allows the creation ? + * + * @returns true, if the entity can be created. false, otherwise. + */ + canCreate(): boolean { return true; } + + /** + * Method that evaluates if the @Input() {entity} can be read. This must answer the question: + * - The entity state (or any other business rule) allows the entity to be read ? + * + * @template IE - The entity domain type to evaluate. + * @param entity - The entity instance to evaluate + * @returns true, if the entity can be read. false, otherwise. + */ + canRead(entity: IE): boolean { return true; } + + /** + * Method that evaluates if the @Input() {entity} can be updated. This must answer the question: + * - The entity state (or any other business rule) allows the entity to be updated ? + * + * @template IE - The entity domain type to evaluate. + * @param entity - The entity instance to evaluate + * @returns true, if the entity can be deleted. false, otherwise. + */ + canUpdate(entity: IE): boolean { return true; } + + /** + * Method that evaluates if the @Input() {entity} can be deleted. This must answer the questions: + * - The entity state (or any other business rule) allows the entity to be deleted ? + * + * @template IE - The entity domain type to evaluate. + * @param entity - The entity instance to evaluate + * @returns true, if the entity can be deleted. false, otherwise. + */ + canDelete(entity: IE): boolean { return true; } +} diff --git a/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.html b/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.html new file mode 100644 index 0000000..445f79b --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.html @@ -0,0 +1,16 @@ +
  • + + + + + +
  • \ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.scss b/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.scss new file mode 100644 index 0000000..5855482 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.scss @@ -0,0 +1,10 @@ +:host li { + display: inline-flex; +} + +select { + height: 24px; + line-height: 24px; + margin-top: 8px; + padding: 0px; +} \ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.ts b/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.ts new file mode 100644 index 0000000..1b97cd3 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/organizations/resilient-organizations.component.ts @@ -0,0 +1,91 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { Observable, tap } from 'rxjs'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import SharedModule from 'app/shared/shared.module'; + +import { ResilientEnvironmentService, EntityArrayResponseType } from 'app/resilient/resilient-environment/service/resilient-environment.service'; +import { OrganizationService } from 'app/entities/organization/service/organization.service'; +import { IOrganization } from 'app/entities/organization/organization.model'; +import { AccountService } from 'app/core/auth/account.service'; + +@Component({ + selector: 'jhi-resilient-organizations', + standalone: true, + imports: [SharedModule,FormsModule, ReactiveFormsModule], + templateUrl: './resilient-organizations.component.html', + styleUrl: './resilient-organizations.component.scss' +}) +export class ResilientOrganizationsComponent implements OnInit { + organizations?: IOrganization[]; + selectedOrganization?: IOrganization; + isLoading = false; + + sortState = sortStateSignal({}); + + protected organizationService = inject(OrganizationService); + protected environmentService = inject(ResilientEnvironmentService); + protected sortService = inject(SortService); + protected accountService = inject(AccountService); + + compareOrganization = (o1: IOrganization | null, o2: IOrganization | null): boolean => + this.organizationService.compareEntity(o1, o2); + + ngOnInit(): void { + if (!this.organizations || this.organizations.length === 0) { + this.load(); + } + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + onChange(event: any): void { + this.environmentService.selectWorkOrganization(this.selectedOrganization??null); + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.environmentService.organizations(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.organizations = this.refineData(dataFromBody); + + if (this.organizations.length >= 1) { + + const parent: IOrganization | null = this.accountService.getParentOrganization(); + if (parent) { + // Select the user Parent Organization + this.selectedOrganization = this.organizations.find(org => org.id === parent.id); + if (this.selectedOrganization) { + this.environmentService.selectWorkOrganization(this.selectedOrganization); + } + } else { + // Select the first + this.selectedOrganization = this.organizations[0]; + this.environmentService.selectWorkOrganization(this.selectedOrganization); + } + + } + } + + protected refineData(data: IOrganization[]): IOrganization[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IOrganization[] | null): IOrganization[] { + return data ?? []; + } +} diff --git a/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector-search-form.service.ts b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector-search-form.service.ts new file mode 100644 index 0000000..f51643f --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector-search-form.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { SearchFormService } from 'app/entities/shared/shared-search-form.service'; + +export interface ISearchPeriodSelector { + periodId?: number | null; +} + +type SearchPeriodSelector = ISearchPeriodSelector; + +type PeriodSelectorSearchFormGroupInput = SearchPeriodSelector; + +type PeriodSelectorSearchFormDefaults = SearchPeriodSelector; + +type PeriodSelectorSearchFormGroupContent = { + periodId: FormControl; +}; + +export type PeriodSelectorSearchFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class PeriodSelectorSearchFormService extends SearchFormService { + createPeriodSelectorSearchFormGroup(search: PeriodSelectorSearchFormGroupInput = { periodId: null }): PeriodSelectorSearchFormGroup { + const searchRawValue = { + ...this.getSearchFormDefaults(), + ...search, + }; + return new FormGroup({ + periodId: new FormControl(searchRawValue.periodId, { + validators: [Validators.required], + }), + }); + } + + getSearchPeriodSelector(form: PeriodSelectorSearchFormGroup): SearchPeriodSelector { + return form.getRawValue() as SearchPeriodSelector; + } + + resetForm(form: PeriodSelectorSearchFormGroup, search: PeriodSelectorSearchFormGroupInput): void { + const searchRawValue = { ...this.getSearchFormDefaults(), ...search }; + form.reset( + { + ...searchRawValue, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getSearchFormDefaults(): PeriodSelectorSearchFormDefaults { + return { }; + } +} diff --git a/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.html b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.html new file mode 100644 index 0000000..1963b85 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.html @@ -0,0 +1,14 @@ + +
    + + +
    \ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.scss b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.scss new file mode 100644 index 0000000..03240e4 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.scss @@ -0,0 +1,5 @@ +label { + font-weight: bold; + margin-bottom: 5px; + margin-right: 5px; +} \ No newline at end of file diff --git a/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.ts b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.ts new file mode 100644 index 0000000..add08a3 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.ts @@ -0,0 +1,97 @@ +import { Component, Input, OnInit, inject } from '@angular/core'; +import { Observable, tap } from 'rxjs'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import SharedModule from 'app/shared/shared.module'; + +import { ResilientEnvironmentService, EntityArrayResponseType } from 'app/resilient/resilient-environment/service/resilient-environment.service'; +import { PeriodService } from 'app/entities/period/service/period.service'; +import { IPeriod } from 'app/entities/period/period.model'; +import { PeriodSelectorSearchFormService, PeriodSelectorSearchFormGroup } from './period-selector-search-form.service'; + +@Component({ + selector: 'app-period-selector', + standalone: true, + imports: [SharedModule,FormsModule, ReactiveFormsModule], + templateUrl: './period-selector.component.html', + styleUrl: './period-selector.component.scss' +}) +export class PeriodSelectorComponent implements OnInit { + @Input() defaultSelectedPeriodId : any; // The initial selected period + + periods?: IPeriod[]; + selectedPeriod?: IPeriod; + isLoading = false; + + sortState = sortStateSignal({}); + + protected periodService = inject(PeriodService); + protected environmentService = inject(ResilientEnvironmentService); + protected sortService = inject(SortService); + protected periodSelectorSearchFormService = inject(PeriodSelectorSearchFormService); + + searchForm: PeriodSelectorSearchFormGroup = this.periodSelectorSearchFormService.createPeriodSelectorSearchFormGroup(); + + comparePeriod = (o1: IPeriod | null, o2: IPeriod | null): boolean => + this.periodService.compareEntity(o1, o2); + + ngOnInit(): void { + console.log(this.defaultSelectedPeriodId); + + if (!this.periods || this.periods.length === 0) { + this.load(); + } + } + + onSearch(): void { + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + onChange(event: any): void { + this.environmentService.selectPeriod(this.selectedPeriod??null); + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + eagerload: true, + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.periodService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.periods = this.refineData(dataFromBody); + if (this.periods.length >= 1) { + if (this.defaultSelectedPeriodId) { + // Select the provided default Period + this.selectedPeriod = this.periods.find(period => period.id === this.defaultSelectedPeriodId) || undefined; + } else { + // Select the first OR the default when provided + this.selectedPeriod = this.periods[0]; + } + + this.environmentService.selectPeriod(this.selectedPeriod ?? null); + if (this.selectedPeriod) { + this.searchForm.patchValue({ periodId: this.selectedPeriod.id }); + } + } + } + + protected refineData(data: IPeriod[]): IPeriod[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IPeriod[] | null): IPeriod[] { + return data ?? []; + } +} diff --git a/src/main/webapp/app/resilient/resilient-environment/service/resilient-environment.service.spec.ts b/src/main/webapp/app/resilient/resilient-environment/service/resilient-environment.service.spec.ts new file mode 100644 index 0000000..9807581 --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/service/resilient-environment.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ResilientEnvironmentService } from './resilient-environment.service'; + +describe('ResilientEnvironmentService', () => { + let service: ResilientEnvironmentService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ResilientEnvironmentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/main/webapp/app/resilient/resilient-environment/service/resilient-environment.service.ts b/src/main/webapp/app/resilient/resilient-environment/service/resilient-environment.service.ts new file mode 100644 index 0000000..313d37e --- /dev/null +++ b/src/main/webapp/app/resilient/resilient-environment/service/resilient-environment.service.ts @@ -0,0 +1,38 @@ +import { inject, Injectable } from '@angular/core'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { IOrganization, } from 'app/entities/organization/organization.model'; +import { IPeriod } from 'app/entities/period/period.model'; +import { createRequestOption } from 'app/core/request/request-util'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { HttpClient, HttpResponse } from '@angular/common/http'; + +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ + providedIn: 'root' +}) +export class ResilientEnvironmentService { + private sourceWorkOrganization = new BehaviorSubject(undefined); + selectedWorkOrganization = this.sourceWorkOrganization.asObservable(); + + private sourcePeriod = new BehaviorSubject(undefined); + selectedPeriod = this.sourcePeriod.asObservable(); + + protected http = inject(HttpClient); + protected applicationConfigService = inject(ApplicationConfigService); + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/env'); + + organizations(req?: any): Observable { + const options = createRequestOption(req); + return this.http.get(`${this.resourceUrl}/organizations`, { params: options, observe: 'response' }); + } + + selectWorkOrganization(selectedOrganization: IOrganization | null):void { + this.sourceWorkOrganization.next(selectedOrganization??null); + } + + selectPeriod(selectedPeriod: IPeriod | null):void { + this.sourcePeriod.next(selectedPeriod??null); + } + +} diff --git a/src/main/webapp/app/security/resources.model.ts b/src/main/webapp/app/security/resources.model.ts new file mode 100644 index 0000000..01cee42 --- /dev/null +++ b/src/main/webapp/app/security/resources.model.ts @@ -0,0 +1,22 @@ +export enum Resources { + CONTENT_PAGE = 'com.oguerreiro.resilient.domain.ContentPage', + DASHBOARD_COMPONENT = 'com.oguerreiro.resilient.domain.DashboardComponent', + DOCUMENT = 'com.oguerreiro.resilient.domain.Document', + INPUT_DATA = 'com.oguerreiro.resilient.domain.InputData', + INPUT_DATA_UPLOAD = 'com.oguerreiro.resilient.domain.InputDataUpload', + INPUT_DATA_UPLOAD_LOG = 'com.oguerreiro.resilient.domain.InputDataUploadLog', + EMISSION_FACTOR = 'com.oguerreiro.resilient.domain.EmissionFactor', + METADATA_PROPERTY = 'com.oguerreiro.resilient.domain.MetadataProperty', + METADATA_VALUE = 'com.oguerreiro.resilient.domain.MetadataValue', + ORGANIZATION = 'com.oguerreiro.resilient.domain.Organization', + OUTPUT_DATA = 'com.oguerreiro.resilient.domain.OutputData', + ORGANIZATION_TYPE = 'com.oguerreiro.resilient.domain.OrganizationType', + PERIOD = 'com.oguerreiro.resilient.domain.Period', + UNIT = 'com.oguerreiro.resilient.domain.Unit', + UNIT_CONVERTER = 'com.oguerreiro.resilient.domain.UnitConverter', + UNIT_TYPE = 'com.oguerreiro.resilient.domain.UnitType', + VARIABLE = 'com.oguerreiro.resilient.domain.Variable', + VARIABLE_CATEGORY = 'com.oguerreiro.resilient.domain.VariableCategory', + VARIABLE_CLASS_TYPE = 'com.oguerreiro.resilient.domain.VariableClassType', + VARIABLE_SCOPE = 'com.oguerreiro.resilient.domain.VariableScope', +} diff --git a/src/main/webapp/app/security/security-action.model.ts b/src/main/webapp/app/security/security-action.model.ts new file mode 100644 index 0000000..1ba0e24 --- /dev/null +++ b/src/main/webapp/app/security/security-action.model.ts @@ -0,0 +1,11 @@ +export enum SecurityAction { + CREATE = 'CREATE', + READ = 'READ', + UPDATE = 'UPDATE', + DELETE = 'DELETE', + + C = 'CREATE', + R = 'READ', + U = 'UPDATE', + D = 'DELETE', +} diff --git a/src/main/webapp/app/security/security-group/delete/security-group-delete-dialog.component.html b/src/main/webapp/app/security/security-group/delete/security-group-delete-dialog.component.html new file mode 100644 index 0000000..345afde --- /dev/null +++ b/src/main/webapp/app/security/security-group/delete/security-group-delete-dialog.component.html @@ -0,0 +1,24 @@ +@if (securityGroup) { +
    + + + + + +
    +} diff --git a/src/main/webapp/app/security/security-group/delete/security-group-delete-dialog.component.ts b/src/main/webapp/app/security/security-group/delete/security-group-delete-dialog.component.ts new file mode 100644 index 0000000..c77d249 --- /dev/null +++ b/src/main/webapp/app/security/security-group/delete/security-group-delete-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { ISecurityGroup } from '../security-group.model'; +import { SecurityGroupService } from '../service/security-group.service'; + +@Component({ + standalone: true, + templateUrl: './security-group-delete-dialog.component.html', + imports: [SharedModule, FormsModule], +}) +export class SecurityGroupDeleteDialogComponent { + securityGroup?: ISecurityGroup; + + protected securityGroupService = inject(SecurityGroupService); + protected activeModal = inject(NgbActiveModal); + + cancel(): void { + this.activeModal.dismiss(); + } + + confirmDelete(id: number): void { + this.securityGroupService.delete(id).subscribe(() => { + this.activeModal.close(ITEM_DELETED_EVENT); + }); + } +} diff --git a/src/main/webapp/app/security/security-group/list/security-group.component.html b/src/main/webapp/app/security/security-group/list/security-group.component.html new file mode 100644 index 0000000..f0e75f5 --- /dev/null +++ b/src/main/webapp/app/security/security-group/list/security-group.component.html @@ -0,0 +1,86 @@ + +
    +

    + Security Group + +
    + + + +
    +

    + + + + + + @if (securityGroups && securityGroups.length >= 0) { +
    + + + + + + + + + + + @for (securityGroup of securityGroups; track trackId) { + + + + + + + } + +
    +
    + Code + + +
    +
    +
    + Name + + +
    +
    {{ securityGroup.code }}{{ securityGroup.name }} +
    + + + Visualizar + + + + + Editar + + +
    +
    +
    + } + + @if (securityGroups?.length === 0) { +
    + Nenhum Security Group encontrado +
    + } +
    diff --git a/src/main/webapp/app/security/security-group/list/security-group.component.ts b/src/main/webapp/app/security/security-group/list/security-group.component.ts new file mode 100644 index 0000000..c7c80c9 --- /dev/null +++ b/src/main/webapp/app/security/security-group/list/security-group.component.ts @@ -0,0 +1,117 @@ +import { Component, NgZone, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import SharedModule from 'app/shared/shared.module'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { FormsModule } from '@angular/forms'; +import { SORT, ITEM_DELETED_EVENT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; +import { ISecurityGroup } from '../security-group.model'; +import { EntityArrayResponseType, SecurityGroupService } from '../service/security-group.service'; +import { SecurityGroupDeleteDialogComponent } from '../delete/security-group-delete-dialog.component'; + +@Component({ + standalone: true, + selector: 'jhi-security-group', + templateUrl: './security-group.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + ], +}) +export class SecurityGroupComponent implements OnInit { + subscription: Subscription | null = null; + securityGroups?: ISecurityGroup[]; + isLoading = false; + + sortState = sortStateSignal({}); + + public router = inject(Router); + protected securityGroupService = inject(SecurityGroupService); + protected activatedRoute = inject(ActivatedRoute); + protected sortService = inject(SortService); + protected modalService = inject(NgbModal); + protected ngZone = inject(NgZone); + + trackId = (_index: number, item: ISecurityGroup): number => this.securityGroupService.getEntityIdentifier(item); + + ngOnInit(): void { + this.subscription = combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]) + .pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + tap(() => { + if (!this.securityGroups || this.securityGroups.length === 0) { + this.load(); + } + }), + ) + .subscribe(); + } + + delete(securityGroup: ISecurityGroup): void { + const modalRef = this.modalService.open(SecurityGroupDeleteDialogComponent, { size: 'lg', backdrop: 'static' }); + modalRef.componentInstance.securityGroup = securityGroup; + // unsubscribe not needed because closed completes on modal close + modalRef.closed + .pipe( + filter(reason => reason === ITEM_DELETED_EVENT), + tap(() => this.load()), + ) + .subscribe(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(event: SortState): void { + this.handleNavigation(event); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data[DEFAULT_SORT_DATA])); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.securityGroups = this.refineData(dataFromBody); + } + + protected refineData(data: ISecurityGroup[]): ISecurityGroup[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: ISecurityGroup[] | null): ISecurityGroup[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + const queryObject: any = { + sort: this.sortService.buildSortParam(this.sortState()), + }; + return this.securityGroupService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(sortState: SortState): void { + const queryParamsObj = { + sort: this.sortService.buildSortParam(sortState), + }; + + this.ngZone.run(() => { + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }); + }); + } +} diff --git a/src/main/webapp/app/security/security-group/route/security-group-routing-resolve.service.ts b/src/main/webapp/app/security/security-group/route/security-group-routing-resolve.service.ts new file mode 100644 index 0000000..75791a2 --- /dev/null +++ b/src/main/webapp/app/security/security-group/route/security-group-routing-resolve.service.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { ISecurityGroup } from '../security-group.model'; +import { SecurityGroupService } from '../service/security-group.service'; + +const securityGroupResolve = (route: ActivatedRouteSnapshot): Observable => { + const id = route.params['id']; + if (id) { + return inject(SecurityGroupService) + .find(id) + .pipe( + mergeMap((securityGroup: HttpResponse) => { + if (securityGroup.body) { + const ut = of(securityGroup.body); + return of(securityGroup.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + ); + } + return of(null); +}; + +export default securityGroupResolve; diff --git a/src/main/webapp/app/security/security-group/security-group-permission.model.ts b/src/main/webapp/app/security/security-group/security-group-permission.model.ts new file mode 100644 index 0000000..2686145 --- /dev/null +++ b/src/main/webapp/app/security/security-group/security-group-permission.model.ts @@ -0,0 +1,15 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; +import { SecurityPermission } from './../security-permission.model'; +import { ISecurityResource } from './../security-resource/security-resource.model'; +import { ISecurityGroup } from './security-group.model'; + +export interface ISecurityGroupPermission extends IResilientEntity { + securityGroup?: Pick | null; + securityResource?: Pick | null; + createPermission?: SecurityPermission | null; + readPermission?: SecurityPermission | null; + updatePermission?: SecurityPermission | null; + deletePermission?: SecurityPermission | null; +} + +export type NewSecurityGroupPermission = Omit & { id: null }; diff --git a/src/main/webapp/app/security/security-group/security-group.model.ts b/src/main/webapp/app/security/security-group/security-group.model.ts new file mode 100644 index 0000000..081aede --- /dev/null +++ b/src/main/webapp/app/security/security-group/security-group.model.ts @@ -0,0 +1,10 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; +import { ISecurityGroupPermission } from './security-group-permission.model'; + +export interface ISecurityGroup extends IResilientEntity { + code?: string | null; + name?: string | null; + securityGroupPermissions?: ISecurityGroupPermission[] | null; +} + +export type NewSecurityGroup = Omit & { id: null }; diff --git a/src/main/webapp/app/security/security-group/security-group.routes.ts b/src/main/webapp/app/security/security-group/security-group.routes.ts new file mode 100644 index 0000000..ea28307 --- /dev/null +++ b/src/main/webapp/app/security/security-group/security-group.routes.ts @@ -0,0 +1,59 @@ +import { Routes } from '@angular/router'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; +import { ASC } from 'app/config/navigation.constants'; + +import { SecurityGroupComponent } from './list/security-group.component'; +import { SecurityGroupUpdateComponent } from './update/security-group-update.component'; +/* +import { SecurityGroupPreviewComponent } from './preview/security-group-preview.component'; +import { SecurityGroupDetailComponent } from './detail/security-group-detail.component'; +*/ +import SecurityGroupResolve from './route/security-group-routing-resolve.service'; + +const securityGroupRoute: Routes = [ + { + path: '', + component: SecurityGroupComponent, + data: { + defaultSort: 'id,' + ASC, + }, + canActivate: [UserRouteAccessService], + }, +/* + { + path: ':id/view', + component: DashboardComponentDetailComponent, + resolve: { + dashboardComponent: DashboardComponentResolve, + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':id/preview', + component: DashboardComponentPreviewComponent, + resolve: { + dashboardComponent: DashboardComponentResolve, + }, + canActivate: [UserRouteAccessService], + }, +*/ + { + path: 'new', + component: SecurityGroupUpdateComponent, + resolve: { + securityGroup: SecurityGroupResolve, + }, + canActivate: [UserRouteAccessService], + }, + { + path: ':id/edit', + component: SecurityGroupUpdateComponent, + resolve: { + securityGroup: SecurityGroupResolve, + }, + canActivate: [UserRouteAccessService], + }, +]; + +export default securityGroupRoute; diff --git a/src/main/webapp/app/security/security-group/service/security-group.service.ts b/src/main/webapp/app/security/security-group/service/security-group.service.ts new file mode 100644 index 0000000..ba66eb5 --- /dev/null +++ b/src/main/webapp/app/security/security-group/service/security-group.service.ts @@ -0,0 +1,28 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { ISecurityGroup, NewSecurityGroup } from '../security-group.model'; +import { ISecurityGroupPermission } from '../security-group-permission.model'; + +export type PartialUpdateSecurityGroup = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class SecurityGroupService extends ResilientService { + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/security/groups'); + getResourceUrl(): string { return this.resourceUrl; } + + // Get a list of UnitConverters, that applies to fromUnit and toUnit + queryPermissions(securityGroupId: number): Observable> { + return this.http.get(`${this.resourceUrl}/${securityGroupId}/permissions`, { observe: 'response' }); + } +} diff --git a/src/main/webapp/app/security/security-group/update/security-group-form.service.ts b/src/main/webapp/app/security/security-group/update/security-group-form.service.ts new file mode 100644 index 0000000..22b8c72 --- /dev/null +++ b/src/main/webapp/app/security/security-group/update/security-group-form.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms'; + +import { ISecurityGroup, NewSecurityGroup } from '../security-group.model'; +import { SecurityGroupPermissionFormGroup, SecurityGroupPermissionFormService } from './security-group-permission-form.service'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts IDashboardComponent for edit and NewDashboardComponentFormGroupInput for create. + */ +type SecurityGroupFormGroupInput = ISecurityGroup | PartialWithRequiredKeyOf; + +type SecurityGroupFormDefaults = Pick; + +type SecurityGroupFormGroupContent = { + id: FormControl; + code: FormControl; + name: FormControl; + securityGroupPermissions: FormArray; + version: FormControl; +}; + +export type SecurityGroupFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class SecurityGroupFormService { + constructor(private fb: FormBuilder, + private securityGroupPermissionFormService: SecurityGroupPermissionFormService ) {} + + createSecurityGroupFormGroup(securityGroup: SecurityGroupFormGroupInput = { id: null }): SecurityGroupFormGroup { + const securityGroupRawValue = { + ...this.getFormDefaults(), + ...securityGroup, + }; + + const items = securityGroup?.securityGroupPermissions??[]; + + return new FormGroup({ + id: new FormControl( + { value: securityGroupRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + code: new FormControl(securityGroupRawValue.code, { + validators: [Validators.required], + }), + name: new FormControl(securityGroupRawValue.name, { + validators: [Validators.required], + }), + securityGroupPermissions: new FormArray(items.map(item => this.securityGroupPermissionFormService.createSecurityGroupPermissionFormGroup(item))), + version: new FormControl(securityGroupRawValue.version), + }); + } + + getSecurityGroup(form: SecurityGroupFormGroup): ISecurityGroup | NewSecurityGroup { + return form.getRawValue() as ISecurityGroup | NewSecurityGroup; + } + + resetForm(form: SecurityGroupFormGroup, securityGroup: SecurityGroupFormGroupInput): void { + const securityGroupRawValue = { + ...this.getFormDefaults(), + ...securityGroup + }; + form.reset( + { + ...securityGroupRawValue, + id: { value: securityGroupRawValue.id, disabled: true }, + } as any /* cast to workaround https://github.com/angular/angular/issues/46458 */, + ); + } + + private getFormDefaults(): SecurityGroupFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/security/security-group/update/security-group-permission-form.service.ts b/src/main/webapp/app/security/security-group/update/security-group-permission-form.service.ts new file mode 100644 index 0000000..e76e506 --- /dev/null +++ b/src/main/webapp/app/security/security-group/update/security-group-permission-form.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { ISecurityGroupPermission, NewSecurityGroupPermission } from '../security-group-permission.model'; + +/** + * A partial Type with required key is used as form input. + */ +type PartialWithRequiredKeyOf = Partial> & { id: T['id'] }; + +/** + * Type for createFormGroup and resetForm argument. + * It accepts ISecurityGroupPermission for edit and NewSecurityGroupPermissionFormGroupInput for create. + */ +type SecurityGroupPermissionFormGroupInput = ISecurityGroupPermission | PartialWithRequiredKeyOf; + +type SecurityGroupPermissionFormDefaults = Pick; + +type SecurityGroupPermissionFormGroupContent = { + id: FormControl; + version: FormControl; + securityGroup: FormControl; + securityResource: FormControl; + createPermission: FormControl; + readPermission: FormControl; + updatePermission: FormControl; + deletePermission: FormControl; +}; + +export type SecurityGroupPermissionFormGroup = FormGroup; + +@Injectable({ providedIn: 'root' }) +export class SecurityGroupPermissionFormService { + createSecurityGroupPermissionFormGroup(securityGroupPermission: SecurityGroupPermissionFormGroupInput = { id: null }): SecurityGroupPermissionFormGroup { + const securityGroupPermissionRawValue = { + ...this.getFormDefaults(), + ...securityGroupPermission, + }; + + return new FormGroup({ + id: new FormControl( + { value: securityGroupPermissionRawValue.id, disabled: true }, + { + nonNullable: true, + validators: [Validators.required], + }, + ), + version: new FormControl(securityGroupPermissionRawValue.version), + + securityGroup: new FormControl(securityGroupPermissionRawValue.securityGroup), + securityResource: new FormControl(securityGroupPermissionRawValue.securityResource), + createPermission: new FormControl(securityGroupPermissionRawValue.createPermission), + readPermission: new FormControl(securityGroupPermissionRawValue.readPermission), + updatePermission: new FormControl(securityGroupPermissionRawValue.updatePermission), + deletePermission: new FormControl(securityGroupPermissionRawValue.deletePermission), + + }); + } + + private getFormDefaults(): SecurityGroupPermissionFormDefaults { + return { + id: null, + }; + } +} diff --git a/src/main/webapp/app/security/security-group/update/security-group-update.component.css b/src/main/webapp/app/security/security-group/update/security-group-update.component.css new file mode 100644 index 0000000..15d60fd --- /dev/null +++ b/src/main/webapp/app/security/security-group/update/security-group-update.component.css @@ -0,0 +1,7 @@ +table.table-security-group-permissions select { + padding: 0.200em; +} + +.table-security-group-permissions > :not(caption) > * > * { + padding: 5px 0px 5px 5px; +} \ No newline at end of file diff --git a/src/main/webapp/app/security/security-group/update/security-group-update.component.html b/src/main/webapp/app/security/security-group/update/security-group-update.component.html new file mode 100644 index 0000000..9d8bbf4 --- /dev/null +++ b/src/main/webapp/app/security/security-group/update/security-group-update.component.html @@ -0,0 +1,144 @@ +
    +
    +
    +

    + Criar ou editar Security Group +

    + +
    + +
    + + @if (editForm.controls.id.value == null) { + + } @else { + + } + + @if (editForm.get('code')!.invalid && (editForm.get('code')!.dirty || editForm.get('code')!.touched)) { +
    + @if (editForm.get('code')?.errors?.required) { + O campo é obrigatório. + } + @if (editForm.get('code')?.errors?.minlength) { + Este campo deve ter pelo menos 3 caracteres. + } +
    + } +
    + +
    + + + @if (editForm.get('name')!.invalid && (editForm.get('name')!.dirty || editForm.get('name')!.touched)) { +
    + @if (editForm.get('name')?.errors?.required) { + O campo é obrigatório. + } +
    + } +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    ResourceCreateReadUpdateDelete
    + {{ permission.get('securityResource')?.value?.name }} + + + + + + + + + +
    +
    +
    + +
    + +
    + + + +
    +
    +
    +
    diff --git a/src/main/webapp/app/security/security-group/update/security-group-update.component.ts b/src/main/webapp/app/security/security-group/update/security-group-update.component.ts new file mode 100644 index 0000000..b95eaac --- /dev/null +++ b/src/main/webapp/app/security/security-group/update/security-group-update.component.ts @@ -0,0 +1,168 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { finalize, map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule, ReactiveFormsModule, FormArray } from '@angular/forms'; + +import { ISecurityResource } from 'app/security/security-resource/security-resource.model'; +import { ISecurityGroup } from './../security-group.model'; +import { ISecurityGroupPermission, NewSecurityGroupPermission } from './../security-group-permission.model'; +import { SecurityGroupService } from '../service/security-group.service'; +import { SecurityResourceService } from 'app/security/security-resource/service/security-resource.service'; +import { SecurityGroupFormService, SecurityGroupFormGroup } from './security-group-form.service'; +import { SecurityGroupPermissionFormService } from './security-group-permission-form.service'; +import { SecurityPermission } from 'app/security/security-permission.model'; + +@Component({ + standalone: true, + selector: 'jhi-security-group-update', + templateUrl: './security-group-update.component.html', + styleUrl: './security-group-update.component.css', + imports: [SharedModule, FormsModule, ReactiveFormsModule], +}) +export class SecurityGroupUpdateComponent implements OnInit { + isSaving = false; + securityGroup: ISecurityGroup | null = null; + currentTab: string = 'permissions'; // default tab + securityPermissions = Object.keys(SecurityPermission); + + protected securityGroupService = inject(SecurityGroupService); + protected securityResourceService = inject(SecurityResourceService); + protected securityGroupFormService = inject(SecurityGroupFormService); + protected securityGroupPermissionFormService = inject(SecurityGroupPermissionFormService); + protected activatedRoute = inject(ActivatedRoute); + + // eslint-disable-next-line @typescript-eslint/member-ordering + editForm: SecurityGroupFormGroup = this.securityGroupFormService.createSecurityGroupFormGroup(); + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ securityGroup }) => { + this.securityGroup = securityGroup; + if (securityGroup) { + // Editing + this.updateForm(securityGroup); + + // Load SecurityGroupPermission's + this.loadSecurityGroupPermissions(); + } else { + // New Group + // Load empty (default) SecurityGroupPermission's + this.loadNewSecurityGroupPermissions(); + } + + this.loadRelationshipsOptions(); + }); + } + + previousState(): void { + window.history.back(); + } + + save(): void { + this.isSaving = true; + const securityGroup = this.securityGroupFormService.getSecurityGroup(this.editForm); + if (securityGroup.id !== null) { + this.subscribeToSaveResponse(this.securityGroupService.update(securityGroup)); + } else { + this.subscribeToSaveResponse(this.securityGroupService.create(securityGroup)); + } + } + + setTab(tabId: string): void { + this.currentTab = tabId; + } + + /* Nested collection methods : SecurityGroupPermission's */ + /* Use it like a property : this.securityGroupPermissions */ + get securityGroupPermissions(): FormArray { + return this.editForm.get('securityGroupPermissions') as FormArray; + } + + removeSecurityGroupPermission(index: number): void { + this.securityGroupPermissions.removeAt(index); + } + + protected subscribeToSaveResponse(result: Observable>): void { + result.pipe(finalize(() => this.onSaveFinalize())).subscribe({ + next: () => this.onSaveSuccess(), + error: () => this.onSaveError(), + }); + } + + protected onSaveSuccess(): void { + this.previousState(); + } + + protected onSaveError(): void { + // Api for inheritance. + } + + protected onSaveFinalize(): void { + this.isSaving = false; + } + + protected updateForm(securityGroup: ISecurityGroup): void { + this.securityGroup = securityGroup; + this.securityGroupFormService.resetForm(this.editForm, securityGroup); + + securityGroup.securityGroupPermissions?.forEach(securityGroupPermission => { + this.securityGroupPermissions.push(this.securityGroupPermissionFormService.createSecurityGroupPermissionFormGroup(securityGroupPermission)); + }); + } + + protected loadRelationshipsOptions(): void { + + } + + protected loadSecurityGroupPermissions(): void { + if (this.securityGroup) { + //Load all SecurityGroupPermissions associated to this SecurityGroup + this.securityGroupService.queryPermissions(this.securityGroup.id).subscribe({ + next: (resources: HttpResponse) => { + const permissions: ISecurityGroupPermission[] | null = resources.body; + + // For each SecurityResource + permissions?.forEach(permission => { + // #2. create a new SecurityGroupPermissionForm + const securityGroupPermissionFormGroup = this.securityGroupPermissionFormService.createSecurityGroupPermissionFormGroup(permission) + + // #3. add to this.securityGroupPermissions + this.securityGroupPermissions.push(securityGroupPermissionFormGroup); + }); + }, + }); + } + } + + protected loadNewSecurityGroupPermissions(): void { + //Load all SecurityResources + this.securityResourceService.query().subscribe({ + next: (resources: HttpResponse) => { + const securityResources: ISecurityResource[] | null = resources.body; + + // For each SecurityResource + securityResources?.forEach(resource => { + // #1. create a new NewSecurityGroupPermission + const newSecurityGroupPermission: NewSecurityGroupPermission = { + id: null, + securityResource: resource, + createPermission: resource.defaultCreatePermission, + readPermission: resource.defaultReadPermission, + updatePermission: resource.defaultUpdatePermission, + deletePermission: resource.defaultDeletePermission + }; + + // #2. create a new SecurityGroupPermissionForm + const newSecurityGroupPermissionFormGroup = this.securityGroupPermissionFormService.createSecurityGroupPermissionFormGroup(newSecurityGroupPermission) + + // #3. add to this.securityGroupPermissions + this.securityGroupPermissions.push(newSecurityGroupPermissionFormGroup); + }); + }, + }); + + } +} diff --git a/src/main/webapp/app/security/security-permission.model.ts b/src/main/webapp/app/security/security-permission.model.ts new file mode 100644 index 0000000..b5acf00 --- /dev/null +++ b/src/main/webapp/app/security/security-permission.model.ts @@ -0,0 +1,7 @@ +export enum SecurityPermission { + ALL = 'ALL', + + NONE = 'NONE', + + HIERARCHY = 'HIERARCHY', +} diff --git a/src/main/webapp/app/security/security-resource-type.model.ts b/src/main/webapp/app/security/security-resource-type.model.ts new file mode 100644 index 0000000..9ec154d --- /dev/null +++ b/src/main/webapp/app/security/security-resource-type.model.ts @@ -0,0 +1,7 @@ +export enum SecurityResourceType { + DOMAIN = 'DOMAIN', + + ACTIVITY = 'ACTIVITY', + + CUSTOM = 'CUSTOM', +} diff --git a/src/main/webapp/app/security/security-resource/security-resource.model.ts b/src/main/webapp/app/security/security-resource/security-resource.model.ts new file mode 100644 index 0000000..de70028 --- /dev/null +++ b/src/main/webapp/app/security/security-resource/security-resource.model.ts @@ -0,0 +1,15 @@ +import { IResilientEntity } from 'app/resilient/resilient-base/resilient-base.model'; +import { SecurityResourceType } from './../security-resource-type.model'; +import { SecurityPermission } from './../security-permission.model'; + +export interface ISecurityResource extends IResilientEntity { + code?: string | null; + name?: string | null; + type?: SecurityResourceType | null; + defaultCreatePermission?: SecurityPermission | null; + defaultReadPermission?: SecurityPermission | null; + defaultUpdatePermission?: SecurityPermission | null; + defaultDeletePermission?: SecurityPermission | null; +} + +export type NewSecurityResource = Omit & { id: null }; diff --git a/src/main/webapp/app/security/security-resource/service/security-resource.service.ts b/src/main/webapp/app/security/security-resource/service/security-resource.service.ts new file mode 100644 index 0000000..be81a83 --- /dev/null +++ b/src/main/webapp/app/security/security-resource/service/security-resource.service.ts @@ -0,0 +1,23 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { isPresent } from 'app/core/util/operators'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ResilientService } from 'app/resilient/resilient-base/resilient-base.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { ISecurityResource, NewSecurityResource } from '../security-resource.model'; + +export type PartialUpdateSecurityGroup = Partial & Pick; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class SecurityResourceService extends ResilientService { + protected applicationConfigService = inject(ApplicationConfigService); + + private resourceUrl = this.applicationConfigService.getEndpointFor('api/security/resources'); + getResourceUrl(): string { return this.resourceUrl; } + +} diff --git a/src/main/webapp/app/security/security.routes.ts b/src/main/webapp/app/security/security.routes.ts new file mode 100644 index 0000000..1528e69 --- /dev/null +++ b/src/main/webapp/app/security/security.routes.ts @@ -0,0 +1,11 @@ +import { Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'security-group', + loadChildren: () => import('./security-group/security-group.routes'), + title: 'securityGroup.home.title', + }, +]; + +export default routes; diff --git a/src/main/webapp/app/shared/alert/alert-error.component.html b/src/main/webapp/app/shared/alert/alert-error.component.html new file mode 100644 index 0000000..75a5c75 --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.component.html @@ -0,0 +1,11 @@ + diff --git a/src/main/webapp/app/shared/alert/alert-error.component.spec.ts b/src/main/webapp/app/shared/alert/alert-error.component.spec.ts new file mode 100644 index 0000000..7c9726e --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.component.spec.ts @@ -0,0 +1,159 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { TranslateModule } from '@ngx-translate/core'; + +import { EventManager } from 'app/core/util/event-manager.service'; +import { Alert, AlertService } from 'app/core/util/alert.service'; + +import { AlertErrorComponent } from './alert-error.component'; + +describe('Alert Error Component', () => { + let comp: AlertErrorComponent; + let fixture: ComponentFixture; + let eventManager: EventManager; + let alertService: AlertService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), AlertErrorComponent], + providers: [EventManager, AlertService], + }) + .overrideTemplate(AlertErrorComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AlertErrorComponent); + comp = fixture.componentInstance; + eventManager = TestBed.inject(EventManager); + alertService = TestBed.inject(AlertService); + alertService.addAlert = (alert: Alert, alerts?: Alert[]) => { + if (alerts) { + alerts.push(alert); + } + return alert; + }; + }); + + describe('Error Handling', () => { + it('Should display an alert on status 0', () => { + // GIVEN + eventManager.broadcast({ name: 'resilientApp.httpError', content: { status: 0 } }); + // THEN + expect(comp.alerts().length).toBe(1); + expect(comp.alerts()[0].translationKey).toBe('error.server.not.reachable'); + }); + + it('Should display an alert on status 404', () => { + // GIVEN + eventManager.broadcast({ name: 'resilientApp.httpError', content: { status: 404 } }); + // THEN + expect(comp.alerts().length).toBe(1); + expect(comp.alerts()[0].translationKey).toBe('error.url.not.found'); + }); + + it('Should display an alert on generic error', () => { + // GIVEN + eventManager.broadcast({ name: 'resilientApp.httpError', content: { error: { message: 'Error Message' } } }); + eventManager.broadcast({ name: 'resilientApp.httpError', content: { error: 'Second Error Message' } }); + // THEN + expect(comp.alerts().length).toBe(2); + expect(comp.alerts()[0].translationKey).toBe('Error Message'); + expect(comp.alerts()[1].translationKey).toBe('Second Error Message'); + }); + + it('Should display an alert on status 400 for generic error', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 400, + statusText: 'Bad Request', + error: { + type: 'https://www.jhipster.tech/problem/problem-with-message', + title: 'Bad Request', + status: 400, + path: '/api/foos', + message: 'error.validation', + }, + }); + eventManager.broadcast({ name: 'resilientApp.httpError', content: response }); + // THEN + expect(comp.alerts().length).toBe(1); + expect(comp.alerts()[0].translationKey).toBe('error.validation'); + }); + + it('Should display an alert on status 400 for generic error without message', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 400, + error: 'Bad Request', + }); + eventManager.broadcast({ name: 'resilientApp.httpError', content: response }); + // THEN + expect(comp.alerts().length).toBe(1); + expect(comp.alerts()[0].translationKey).toBe('Bad Request'); + }); + + it('Should display an alert on status 400 for invalid parameters', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 400, + statusText: 'Bad Request', + error: { + type: 'https://www.jhipster.tech/problem/problem-with-message', + title: 'Method argument not valid', + status: 400, + path: '/api/foos', + message: 'error.validation', + fieldErrors: [{ objectName: 'foo', field: 'minField', message: 'Min' }], + }, + }); + eventManager.broadcast({ name: 'resilientApp.httpError', content: response }); + // THEN + expect(comp.alerts().length).toBe(1); + expect(comp.alerts()[0].translationKey).toBe('error.Size'); + }); + + it('Should display an alert on status 400 for error headers', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders().append('app-error', 'Error Message').append('app-params', 'foo'), + status: 400, + statusText: 'Bad Request', + error: { + status: 400, + message: 'error.validation', + }, + }); + eventManager.broadcast({ name: 'resilientApp.httpError', content: response }); + // THEN + expect(comp.alerts().length).toBe(1); + expect(comp.alerts()[0].translationKey).toBe('Error Message'); + }); + + it('Should display an alert on status 500 with detail', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 500, + statusText: 'Internal server error', + error: { + status: 500, + message: 'error.http.500', + detail: 'Detailed error message', + }, + }); + eventManager.broadcast({ name: 'resilientApp.httpError', content: response }); + // THEN + expect(comp.alerts().length).toBe(1); + expect(comp.alerts()[0].translationKey).toBe('error.http.500'); + }); + }); +}); diff --git a/src/main/webapp/app/shared/alert/alert-error.component.ts b/src/main/webapp/app/shared/alert/alert-error.component.ts new file mode 100644 index 0000000..8b78f63 --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.component.ts @@ -0,0 +1,133 @@ +import { Component, inject, OnDestroy, signal } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateService } from '@ngx-translate/core'; + +import { Alert, AlertService } from 'app/core/util/alert.service'; +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; +import { AlertError } from './alert-error.model'; + +@Component({ + standalone: true, + selector: 'jhi-alert-error', + templateUrl: './alert-error.component.html', + imports: [CommonModule, NgbModule], +}) +export class AlertErrorComponent implements OnDestroy { + alerts = signal([]); + errorListener: Subscription; + httpErrorListener: Subscription; + + private alertService = inject(AlertService); + private eventManager = inject(EventManager); + + private translateService = inject(TranslateService); + + constructor() { + this.errorListener = this.eventManager.subscribe('resilientApp.error', (response: EventWithContent | string) => { + const errorResponse = (response as EventWithContent).content; + this.addErrorAlert(errorResponse.message, errorResponse.key, errorResponse.params); + }); + + this.httpErrorListener = this.eventManager.subscribe('resilientApp.httpError', (response: EventWithContent | string) => { + this.handleHttpError(response); + }); + } + + setClasses(alert: Alert): { [key: string]: boolean } { + const classes = { 'jhi-toast': Boolean(alert.toast) }; + if (alert.position) { + return { ...classes, [alert.position]: true }; + } + return classes; + } + + ngOnDestroy(): void { + this.eventManager.destroy(this.errorListener); + this.eventManager.destroy(this.httpErrorListener); + } + + close(alert: Alert): void { + alert.close?.(this.alerts()); + } + + private addErrorAlert(message?: string, translationKey?: string, translationParams?: { [key: string]: unknown }): void { + this.alertService.addAlert({ type: 'danger', message, translationKey, translationParams }, this.alerts()); + } + + private handleHttpError(response: EventWithContent | string): void { + const httpErrorResponse = (response as EventWithContent).content; + switch (httpErrorResponse.status) { + // connection refused, server not reachable + case 0: + this.addErrorAlert('Server not reachable', 'error.server.not.reachable'); + break; + + case 400: { + this.handleBadRequest(httpErrorResponse); + break; + } + + case 404: + this.addErrorAlert('Not found', 'error.url.not.found'); + break; + + default: + this.handleDefaultError(httpErrorResponse); + } + } + + private handleBadRequest(httpErrorResponse: HttpErrorResponse): void { + const arr = httpErrorResponse.headers.keys(); + let errorHeader: string | null = null; + let entityKey: string | null = null; + for (const entry of arr) { + if (entry.toLowerCase().endsWith('app-error')) { + errorHeader = httpErrorResponse.headers.get(entry); + } else if (entry.toLowerCase().endsWith('app-params')) { + entityKey = httpErrorResponse.headers.get(entry); + } + } + if (errorHeader) { + const alertData = entityKey ? { entityName: this.translateService.instant(`global.menu.entities.${entityKey}`) } : undefined; + this.addErrorAlert(errorHeader, errorHeader, alertData); + } else if (httpErrorResponse.error !== '' && httpErrorResponse.error.fieldErrors) { + this.handleFieldsError(httpErrorResponse); + } else if (httpErrorResponse.error !== '' && httpErrorResponse.error.message) { + this.addErrorAlert( + httpErrorResponse.error.detail ?? httpErrorResponse.error.message, + httpErrorResponse.error.message, + httpErrorResponse.error.params, + ); + } else { + this.addErrorAlert(httpErrorResponse.error, httpErrorResponse.error); + } + } + + private handleDefaultError(httpErrorResponse: HttpErrorResponse): void { + if (httpErrorResponse.error !== '' && httpErrorResponse.error.message) { + this.addErrorAlert( + httpErrorResponse.error.detail ?? httpErrorResponse.error.message, + httpErrorResponse.error.message, + httpErrorResponse.error.params, + ); + } else { + this.addErrorAlert(httpErrorResponse.error, httpErrorResponse.error); + } + } + + private handleFieldsError(httpErrorResponse: HttpErrorResponse): void { + const fieldErrors = httpErrorResponse.error.fieldErrors; + for (const fieldError of fieldErrors) { + if (['Min', 'Max', 'DecimalMin', 'DecimalMax'].includes(fieldError.message)) { + fieldError.message = 'Size'; + } + // convert 'something[14].other[4].id' to 'something[].other[].id' so translations can be written to it + const convertedField: string = fieldError.field.replace(/\[\d*\]/g, '[]'); + const fieldName: string = this.translateService.instant(`resilientApp.${fieldError.objectName as string}.${convertedField}`); + this.addErrorAlert(`Error on field "${fieldName}"`, `error.${fieldError.message as string}`, { fieldName }); + } + } +} diff --git a/src/main/webapp/app/shared/alert/alert-error.model.ts b/src/main/webapp/app/shared/alert/alert-error.model.ts new file mode 100644 index 0000000..cc8ca7d --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.model.ts @@ -0,0 +1,7 @@ +export class AlertError { + constructor( + public message: string, + public key?: string, + public params?: { [key: string]: unknown }, + ) {} +} diff --git a/src/main/webapp/app/shared/alert/alert.component.html b/src/main/webapp/app/shared/alert/alert.component.html new file mode 100644 index 0000000..75a5c75 --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert.component.html @@ -0,0 +1,11 @@ + diff --git a/src/main/webapp/app/shared/alert/alert.component.spec.ts b/src/main/webapp/app/shared/alert/alert.component.spec.ts new file mode 100644 index 0000000..79fe41f --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert.component.spec.ts @@ -0,0 +1,44 @@ +jest.mock('app/core/util/alert.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { AlertService } from 'app/core/util/alert.service'; + +import { AlertComponent } from './alert.component'; + +describe('Alert Component', () => { + let comp: AlertComponent; + let fixture: ComponentFixture; + let mockAlertService: AlertService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [AlertComponent], + providers: [AlertService], + }) + .overrideTemplate(AlertComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AlertComponent); + comp = fixture.componentInstance; + mockAlertService = TestBed.inject(AlertService); + }); + + it('Should call alertService.get on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockAlertService.get).toHaveBeenCalled(); + }); + + it('Should call alertService.clear on destroy', () => { + // WHEN + comp.ngOnDestroy(); + + // THEN + expect(mockAlertService.clear).toHaveBeenCalled(); + }); +}); diff --git a/src/main/webapp/app/shared/alert/alert.component.ts b/src/main/webapp/app/shared/alert/alert.component.ts new file mode 100644 index 0000000..29e1c7b --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert.component.ts @@ -0,0 +1,37 @@ +import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { AlertService, Alert } from 'app/core/util/alert.service'; + +@Component({ + standalone: true, + selector: 'jhi-alert', + templateUrl: './alert.component.html', + imports: [CommonModule, NgbModule], +}) +export class AlertComponent implements OnInit, OnDestroy { + alerts = signal([]); + + private alertService = inject(AlertService); + + ngOnInit(): void { + this.alerts.set(this.alertService.get()); + } + + setClasses(alert: Alert): { [key: string]: boolean } { + const classes = { 'jhi-toast': Boolean(alert.toast) }; + if (alert.position) { + return { ...classes, [alert.position]: true }; + } + return classes; + } + + ngOnDestroy(): void { + this.alertService.clear(); + } + + close(alert: Alert): void { + alert.close?.(this.alerts()); + } +} diff --git a/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts b/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts new file mode 100644 index 0000000..560cb93 --- /dev/null +++ b/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts @@ -0,0 +1,99 @@ +jest.mock('app/core/auth/account.service'); + +import { Component, ElementRef, WritableSignal, signal, viewChild } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; + +import HasAnyAuthorityDirective from './has-any-authority.directive'; + +@Component({ + template: `
    `, +}) +class TestHasAnyAuthorityDirectiveComponent { + content = viewChild('content'); +} + +describe('HasAnyAuthorityDirective tests', () => { + let mockAccountService: AccountService; + let currentAccount: WritableSignal; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HasAnyAuthorityDirective, HttpClientTestingModule, TranslateModule.forRoot()], + declarations: [TestHasAnyAuthorityDirectiveComponent], + providers: [AccountService], + }); + })); + + beforeEach(() => { + mockAccountService = TestBed.inject(AccountService); + currentAccount = signal({ activated: true, authorities: [] } as any); + mockAccountService.trackCurrentAccount = jest.fn(() => currentAccount); + }); + + describe('set jhiHasAnyAuthority', () => { + it('should show restricted content to user if user has required role', () => { + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => true); + const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(comp.content).toBeDefined(); + }); + + it('should not show restricted content to user if user has not required role', () => { + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => false); + const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(comp.content()).toBeUndefined(); + }); + }); + + describe('change authorities', () => { + it('should show or not show restricted content correctly if user authorities are changing', () => { + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn((): boolean => Boolean(currentAccount())); + const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(comp.content()).toBeDefined(); + + // GIVEN + currentAccount.set(null); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(comp.content()).toBeUndefined(); + + // WHEN + currentAccount.set({ activated: true, authorities: ['foo'] } as any); + fixture.detectChanges(); + + // THEN + expect(comp.content).toBeDefined(); + }); + }); +}); diff --git a/src/main/webapp/app/shared/auth/has-any-authority.directive.ts b/src/main/webapp/app/shared/auth/has-any-authority.directive.ts new file mode 100644 index 0000000..1b5e9ce --- /dev/null +++ b/src/main/webapp/app/shared/auth/has-any-authority.directive.ts @@ -0,0 +1,42 @@ +import { Directive, inject, input, TemplateRef, ViewContainerRef, effect, computed } from '@angular/core'; + +import { AccountService } from 'app/core/auth/account.service'; + +/** + * @whatItDoes Conditionally includes an HTML element if current user has any + * of the authorities passed as the `expression`. + * + * @howToUse + * ``` + * ... + * + * ... + * ``` + */ +@Directive({ + standalone: true, + selector: '[jhiHasAnyAuthority]', +}) +export default class HasAnyAuthorityDirective { + public authorities = input([], { alias: 'jhiHasAnyAuthority' }); + + private templateRef = inject(TemplateRef); + private viewContainerRef = inject(ViewContainerRef); + + constructor() { + const accountService = inject(AccountService); + const currentAccount = accountService.trackCurrentAccount(); + const hasPermission = computed(() => currentAccount()?.authorities && accountService.hasAnyAuthority(this.authorities())); + + effect( + () => { + if (hasPermission()) { + this.viewContainerRef.createEmbeddedView(this.templateRef); + } else { + this.viewContainerRef.clear(); + } + }, + { allowSignalWrites: true }, + ); + } +} diff --git a/src/main/webapp/app/shared/date/duration.pipe.ts b/src/main/webapp/app/shared/date/duration.pipe.ts new file mode 100644 index 0000000..fda99e3 --- /dev/null +++ b/src/main/webapp/app/shared/date/duration.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import dayjs from 'dayjs/esm'; + +@Pipe({ + standalone: true, + name: 'duration', +}) +export default class DurationPipe implements PipeTransform { + transform(value: any): string { + if (value) { + return dayjs.duration(value).humanize(); + } + return ''; + } +} diff --git a/src/main/webapp/app/shared/date/format-medium-date.pipe.spec.ts b/src/main/webapp/app/shared/date/format-medium-date.pipe.spec.ts new file mode 100644 index 0000000..bdb618e --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-date.pipe.spec.ts @@ -0,0 +1,19 @@ +import dayjs from 'dayjs/esm'; + +import FormatMediumDatePipe from './format-medium-date.pipe'; + +describe('FormatMediumDatePipe', () => { + const formatMediumDatePipe = new FormatMediumDatePipe(); + + it('should return an empty string when receive undefined', () => { + expect(formatMediumDatePipe.transform(undefined)).toBe(''); + }); + + it('should return an empty string when receive null', () => { + expect(formatMediumDatePipe.transform(null)).toBe(''); + }); + + it('should format date like this D MMM YYYY', () => { + expect(formatMediumDatePipe.transform(dayjs('2020-11-16').locale('fr'))).toBe('16 Nov 2020'); + }); +}); diff --git a/src/main/webapp/app/shared/date/format-medium-date.pipe.ts b/src/main/webapp/app/shared/date/format-medium-date.pipe.ts new file mode 100644 index 0000000..96b679b --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-date.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import dayjs from 'dayjs/esm'; + +@Pipe({ + standalone: true, + name: 'formatMediumDate', +}) +export default class FormatMediumDatePipe implements PipeTransform { + transform(day: dayjs.Dayjs | null | undefined): string { + return day ? day.format('D MMM YYYY') : ''; + } +} diff --git a/src/main/webapp/app/shared/date/format-medium-datetime.pipe.spec.ts b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.spec.ts new file mode 100644 index 0000000..c08aa47 --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.spec.ts @@ -0,0 +1,19 @@ +import dayjs from 'dayjs/esm'; + +import FormatMediumDatetimePipe from './format-medium-datetime.pipe'; + +describe('FormatMediumDatePipe', () => { + const formatMediumDatetimePipe = new FormatMediumDatetimePipe(); + + it('should return an empty string when receive undefined', () => { + expect(formatMediumDatetimePipe.transform(undefined)).toBe(''); + }); + + it('should return an empty string when receive null', () => { + expect(formatMediumDatetimePipe.transform(null)).toBe(''); + }); + + it('should format date like this D MMM YYYY', () => { + expect(formatMediumDatetimePipe.transform(dayjs('2020-11-16').locale('fr'))).toBe('16 Nov 2020 00:00:00'); + }); +}); diff --git a/src/main/webapp/app/shared/date/format-medium-datetime.pipe.ts b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.ts new file mode 100644 index 0000000..bd09cfb --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import dayjs from 'dayjs/esm'; + +@Pipe({ + standalone: true, + name: 'formatMediumDatetime', +}) +export default class FormatMediumDatetimePipe implements PipeTransform { + transform(day: dayjs.Dayjs | null | undefined): string { + return day ? day.format('D MMM YYYY HH:mm:ss') : ''; + } +} diff --git a/src/main/webapp/app/shared/date/index.ts b/src/main/webapp/app/shared/date/index.ts new file mode 100644 index 0000000..5372ce8 --- /dev/null +++ b/src/main/webapp/app/shared/date/index.ts @@ -0,0 +1,3 @@ +export { default as DurationPipe } from './duration.pipe'; +export { default as FormatMediumDatePipe } from './format-medium-date.pipe'; +export { default as FormatMediumDatetimePipe } from './format-medium-datetime.pipe'; diff --git a/src/main/webapp/app/shared/filter/filter.component.html b/src/main/webapp/app/shared/filter/filter.component.html new file mode 100644 index 0000000..fef69f8 --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.component.html @@ -0,0 +1,20 @@ +@if (filters.hasAnyFilterSet()) { +
    + Following filters are set + +
      + @for (filterOption of filters.filterOptions; track filterOption.name) { + @for (value of filterOption.values; track value) { +
    • + {{ filterOption.name }}: {{ value }} + +
    • + } + } +
    +
    +} diff --git a/src/main/webapp/app/shared/filter/filter.component.ts b/src/main/webapp/app/shared/filter/filter.component.ts new file mode 100644 index 0000000..564754a --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import SharedModule from '../shared.module'; +import { IFilterOptions } from './filter.model'; + +@Component({ + standalone: true, + selector: 'jhi-filter', + imports: [SharedModule], + templateUrl: './filter.component.html', +}) +export default class FilterComponent { + @Input() filters!: IFilterOptions; + + clearAllFilters(): void { + this.filters.clear(); + } + + clearFilter(filterName: string, value: string): void { + this.filters.removeFilter(filterName, value); + } +} diff --git a/src/main/webapp/app/shared/filter/filter.model.spec.ts b/src/main/webapp/app/shared/filter/filter.model.spec.ts new file mode 100644 index 0000000..41212ee --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.model.spec.ts @@ -0,0 +1,242 @@ +import { convertToParamMap, ParamMap, Params } from '@angular/router'; +import { FilterOptions, FilterOption } from './filter.model'; + +describe('FilterModel Tests', () => { + describe('FilterOption', () => { + let filterOption: FilterOption; + + beforeEach(() => { + filterOption = new FilterOption('foo', ['bar', 'bar2']); + }); + + it('nameAsQueryParam returns query key', () => { + expect(filterOption.nameAsQueryParam()).toEqual('filter[foo]'); + }); + + describe('addValue', () => { + it('adds multiples unique values and returns true', () => { + const ret = filterOption.addValue('bar2', 'bar3', 'bar4'); + expect(filterOption.values).toMatchObject(['bar', 'bar2', 'bar3', 'bar4']); + expect(ret).toBe(true); + }); + it("doesn't adds duplicated values and return false", () => { + const ret = filterOption.addValue('bar', 'bar2'); + expect(filterOption.values).toMatchObject(['bar', 'bar2']); + expect(ret).toBe(false); + }); + }); + + describe('removeValue', () => { + it('removes the exiting value and return true', () => { + const ret = filterOption.removeValue('bar'); + expect(filterOption.values).toMatchObject(['bar2']); + expect(ret).toBe(true); + }); + it("doesn't removes the value and return false", () => { + const ret = filterOption.removeValue('foo'); + expect(filterOption.values).toMatchObject(['bar', 'bar2']); + expect(ret).toBe(false); + }); + }); + + describe('equals', () => { + it('returns true to matching options', () => { + const otherFilterOption = new FilterOption(filterOption.name, filterOption.values.concat()); + expect(filterOption.equals(otherFilterOption)).toBe(true); + expect(otherFilterOption.equals(filterOption)).toBe(true); + }); + it('returns false to different name', () => { + const otherFilterOption = new FilterOption('bar', filterOption.values.concat()); + expect(filterOption.equals(otherFilterOption)).toBe(false); + expect(otherFilterOption.equals(filterOption)).toBe(false); + }); + it('returns false to different values', () => { + const otherFilterOption = new FilterOption('bar', []); + expect(filterOption.equals(otherFilterOption)).toBe(false); + expect(otherFilterOption.equals(filterOption)).toBe(false); + }); + }); + }); + + describe('FilterOptions', () => { + describe('hasAnyFilterSet', () => { + it('with empty options returns false', () => { + const filters = new FilterOptions(); + expect(filters.hasAnyFilterSet()).toBe(false); + }); + it('with options and empty values returns false', () => { + const filters = new FilterOptions([new FilterOption('foo'), new FilterOption('bar')]); + expect(filters.hasAnyFilterSet()).toBe(false); + }); + it('with option and value returns true', () => { + const filters = new FilterOptions([new FilterOption('foo', ['bar'])]); + expect(filters.hasAnyFilterSet()).toBe(true); + }); + }); + + describe('clear', () => { + it("removes empty filters and doesn't emit next element", () => { + const filters = new FilterOptions([new FilterOption('foo'), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + filters.clear(); + + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([]); + }); + it('removes empty filters and emits next element', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + filters.clear(); + + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([]); + }); + }); + + describe('addFilter', () => { + it('adds a non existing FilterOption, returns true and emit next element', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.addFilter('addedFilter', 'addedValue'); + + expect(result).toBe(true); + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([ + { name: 'foo', values: ['existingFoo1', 'existingFoo2'] }, + { name: 'addedFilter', values: ['addedValue'] }, + ]); + }); + it('adds a non existing value to FilterOption, returns true and emit next element', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.addFilter('foo', 'addedValue1', 'addedValue2'); + + expect(result).toBe(true); + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([ + { name: 'foo', values: ['existingFoo1', 'existingFoo2', 'addedValue1', 'addedValue2'] }, + ]); + }); + it("doesn't add FilterOption values already added, returns false and doesn't emit next element", () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.addFilter('foo', 'existingFoo1', 'existingFoo2'); + + expect(result).toBe(false); + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]); + }); + }); + + describe('removeFilter', () => { + it('removes an existing FilterOptions and returns true', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.removeFilter('foo', 'existingFoo1'); + + expect(result).toBe(true); + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo2'] }]); + }); + it("doesn't remove a non existing FilterOptions values returns false", () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.removeFilter('foo', 'nonExisting1'); + + expect(result).toBe(false); + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]); + }); + it("doesn't remove a non existing FilterOptions returns false", () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.removeFilter('nonExisting', 'nonExisting1'); + + expect(result).toBe(false); + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]); + }); + }); + + describe('initializeFromParams', () => { + const oneValidParam: Params = { + test: 'blub', + 'filter[hello.in]': 'world', + 'filter[invalid': 'invalid', + filter_invalid2: 'invalid', + }; + + const noValidParam: Params = { + test: 'blub', + 'filter[invalid': 'invalid', + filter_invalid2: 'invalid', + }; + + const paramWithTwoValues: Params = { + 'filter[hello.in]': ['world', 'world2'], + }; + + const paramWithTwoKeys: Params = { + 'filter[hello.in]': ['world', 'world2'], + 'filter[hello.notIn]': ['world3', 'world4'], + }; + + it('should parse from Params if there are any and not emit next element', () => { + const filters: FilterOptions = new FilterOptions([new FilterOption('foo', ['bar'])]); + jest.spyOn(filters.filterChanges, 'next'); + const paramMap: ParamMap = convertToParamMap(oneValidParam); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'hello.in', values: ['world'] }]); + }); + + it('should parse from Params and have none if there are none', () => { + const filters: FilterOptions = new FilterOptions(); + const paramMap: ParamMap = convertToParamMap(noValidParam); + jest.spyOn(filters.filterChanges, 'next'); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([]); + }); + + it('should parse from Params and have a parameter with 2 values and one additional value', () => { + const filters: FilterOptions = new FilterOptions([new FilterOption('hello.in', ['world'])]); + jest.spyOn(filters.filterChanges, 'next'); + + const paramMap: ParamMap = convertToParamMap(paramWithTwoValues); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'hello.in', values: ['world', 'world2'] }]); + }); + + it('should parse from Params and have a parameter with 2 keys', () => { + const filters: FilterOptions = new FilterOptions(); + jest.spyOn(filters.filterChanges, 'next'); + + const paramMap: ParamMap = convertToParamMap(paramWithTwoKeys); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([ + { name: 'hello.in', values: ['world', 'world2'] }, + { name: 'hello.notIn', values: ['world3', 'world4'] }, + ]); + }); + }); + }); +}); diff --git a/src/main/webapp/app/shared/filter/filter.model.ts b/src/main/webapp/app/shared/filter/filter.model.ts new file mode 100644 index 0000000..794a0c0 --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.model.ts @@ -0,0 +1,159 @@ +import { ParamMap } from '@angular/router'; +import { Subject } from 'rxjs'; + +export interface IFilterOptions { + readonly filterChanges: Subject; + get filterOptions(): IFilterOption[]; + hasAnyFilterSet(): boolean; + clear(): boolean; + initializeFromParams(params: ParamMap): boolean; + addFilter(name: string, ...values: string[]): boolean; + removeFilter(name: string, value: string): boolean; +} + +export interface IFilterOption { + name: string; + values: string[]; + nameAsQueryParam(): string; +} + +export class FilterOption implements IFilterOption { + constructor( + public name: string, + public values: string[] = [], + ) { + this.values = [...new Set(values)]; + } + + nameAsQueryParam(): string { + return 'filter[' + this.name + ']'; + } + + isSet(): boolean { + return this.values.length > 0; + } + + addValue(...values: string[]): boolean { + const missingValues = values.filter(value => value && !this.values.includes(value)); + if (missingValues.length > 0) { + this.values.push(...missingValues); + return true; + } + return false; + } + + removeValue(value: string): boolean { + const indexOf = this.values.indexOf(value); + if (indexOf === -1) { + return false; + } + + this.values.splice(indexOf, 1); + return true; + } + + clone(): FilterOption { + return new FilterOption(this.name, this.values.concat()); + } + + equals(other: IFilterOption): boolean { + return ( + this.name === other.name && + this.values.length === other.values.length && + this.values.every(thisValue => other.values.includes(thisValue)) && + other.values.every(otherValue => this.values.includes(otherValue)) + ); + } +} + +export class FilterOptions implements IFilterOptions { + readonly filterChanges: Subject = new Subject(); + private _filterOptions: FilterOption[]; + + constructor(filterOptions: FilterOption[] = []) { + this._filterOptions = filterOptions; + } + + get filterOptions(): FilterOption[] { + return this._filterOptions.filter(option => option.isSet()); + } + + hasAnyFilterSet(): boolean { + return this._filterOptions.some(e => e.isSet()); + } + + clear(): boolean { + const hasFields = this.hasAnyFilterSet(); + this._filterOptions = []; + if (hasFields) { + this.changed(); + } + return hasFields; + } + + initializeFromParams(params: ParamMap): boolean { + const oldFilters: FilterOptions = this.clone(); + + this._filterOptions = []; + + const filterRegex = /filter\[(.+)\]/; + params.keys + .filter(paramKey => filterRegex.test(paramKey)) + .forEach(matchingParam => { + const matches = filterRegex.exec(matchingParam); + if (matches && matches.length > 1) { + this.getFilterOptionByName(matches[1], true).addValue(...params.getAll(matchingParam)); + } + }); + + if (oldFilters.equals(this)) { + return false; + } + return true; + } + + addFilter(name: string, ...values: string[]): boolean { + if (this.getFilterOptionByName(name, true).addValue(...values)) { + this.changed(); + return true; + } + return false; + } + + removeFilter(name: string, value: string): boolean { + if (this.getFilterOptionByName(name)?.removeValue(value)) { + this.changed(); + return true; + } + return false; + } + + protected changed(): void { + this.filterChanges.next(this.filterOptions.map(option => option.clone())); + } + + protected equals(other: FilterOptions): boolean { + const thisFilters = this.filterOptions; + const otherFilters = other.filterOptions; + if (thisFilters.length !== otherFilters.length) { + return false; + } + return thisFilters.every(option => other.getFilterOptionByName(option.name)?.equals(option)); + } + + protected clone(): FilterOptions { + return new FilterOptions(this.filterOptions.map(option => new FilterOption(option.name, option.values.concat()))); + } + + protected getFilterOptionByName(name: string, add: true): FilterOption; + protected getFilterOptionByName(name: string, add: false): FilterOption | null; + protected getFilterOptionByName(name: string): FilterOption | null; + protected getFilterOptionByName(name: string, add = false): FilterOption | null { + const addOption = (option: FilterOption): FilterOption => { + this._filterOptions.push(option); + return option; + }; + + return this._filterOptions.find(thisOption => thisOption.name === name) ?? (add ? addOption(new FilterOption(name)) : null); + } +} diff --git a/src/main/webapp/app/shared/filter/index.ts b/src/main/webapp/app/shared/filter/index.ts new file mode 100644 index 0000000..ae0af5a --- /dev/null +++ b/src/main/webapp/app/shared/filter/index.ts @@ -0,0 +1,2 @@ +export { default as FilterComponent } from './filter.component'; +export * from './filter.model'; diff --git a/src/main/webapp/app/shared/language/find-language-from-key.pipe.ts b/src/main/webapp/app/shared/language/find-language-from-key.pipe.ts new file mode 100644 index 0000000..c73c514 --- /dev/null +++ b/src/main/webapp/app/shared/language/find-language-from-key.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + standalone: true, + name: 'findLanguageFromKey', +}) +export default class FindLanguageFromKeyPipe implements PipeTransform { + private languages: { [key: string]: { name: string; rtl?: boolean } } = { + 'pt-pt': { name: 'Português' }, + en: { name: 'English' }, + // jhipster-needle-i18n-language-key-pipe - JHipster will add/remove languages in this object + }; + + transform(lang: string): string { + return this.languages[lang].name; + } +} diff --git a/src/main/webapp/app/shared/language/index.ts b/src/main/webapp/app/shared/language/index.ts new file mode 100644 index 0000000..3446ac2 --- /dev/null +++ b/src/main/webapp/app/shared/language/index.ts @@ -0,0 +1,2 @@ +export { default as TranslateDirective } from './translate.directive'; +export { default as FindLanguageFromKeyPipe } from './find-language-from-key.pipe'; diff --git a/src/main/webapp/app/shared/language/translate.directive.spec.ts b/src/main/webapp/app/shared/language/translate.directive.spec.ts new file mode 100644 index 0000000..b7b5657 --- /dev/null +++ b/src/main/webapp/app/shared/language/translate.directive.spec.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; + +import TranslateDirective from './translate.directive'; + +@Component({ + template: `
    `, +}) +class TestTranslateDirectiveComponent {} + +describe('TranslateDirective Tests', () => { + let fixture: ComponentFixture; + let translateService: TranslateService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), TranslateDirective], + declarations: [TestTranslateDirectiveComponent], + }); + })); + + beforeEach(() => { + translateService = TestBed.inject(TranslateService); + fixture = TestBed.createComponent(TestTranslateDirectiveComponent); + }); + + it('should change HTML', () => { + const spy = jest.spyOn(translateService, 'get'); + + fixture.detectChanges(); + + expect(spy).toHaveBeenCalled(); + }); +}); diff --git a/src/main/webapp/app/shared/language/translate.directive.ts b/src/main/webapp/app/shared/language/translate.directive.ts new file mode 100644 index 0000000..3aae8af --- /dev/null +++ b/src/main/webapp/app/shared/language/translate.directive.ts @@ -0,0 +1,53 @@ +import { inject, Input, Directive, ElementRef, OnChanges, OnInit, OnDestroy } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { translationNotFoundMessage } from 'app/config/translation.config'; + +/** + * A wrapper directive on top of the translate pipe as the inbuilt translate directive from ngx-translate is too verbose and buggy + */ +@Directive({ + standalone: true, + selector: '[jhiTranslate]', +}) +export default class TranslateDirective implements OnChanges, OnInit, OnDestroy { + @Input() jhiTranslate!: string; + @Input() translateValues?: { [key: string]: unknown }; + + private readonly directiveDestroyed = new Subject(); + + private el = inject(ElementRef); + private translateService = inject(TranslateService); + + ngOnInit(): void { + this.translateService.onLangChange.pipe(takeUntil(this.directiveDestroyed)).subscribe(() => { + this.getTranslation(); + }); + this.translateService.onTranslationChange.pipe(takeUntil(this.directiveDestroyed)).subscribe(() => { + this.getTranslation(); + }); + } + + ngOnChanges(): void { + this.getTranslation(); + } + + ngOnDestroy(): void { + this.directiveDestroyed.next(null); + this.directiveDestroyed.complete(); + } + + private getTranslation(): void { + this.translateService + .get(this.jhiTranslate, this.translateValues) + .pipe(takeUntil(this.directiveDestroyed)) + .subscribe({ + next: value => { + this.el.nativeElement.innerHTML = value; + }, + error: () => `${translationNotFoundMessage}[${this.jhiTranslate}]`, + }); + } +} diff --git a/src/main/webapp/app/shared/language/translation.module.ts b/src/main/webapp/app/shared/language/translation.module.ts new file mode 100644 index 0000000..40cb9a9 --- /dev/null +++ b/src/main/webapp/app/shared/language/translation.module.ts @@ -0,0 +1,32 @@ +import { inject, NgModule } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { TranslateModule, TranslateService, TranslateLoader, MissingTranslationHandler } from '@ngx-translate/core'; +import { translatePartialLoader, missingTranslationHandler } from 'app/config/translation.config'; +import { StateStorageService } from 'app/core/auth/state-storage.service'; + +@NgModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: translatePartialLoader, + deps: [HttpClient], + }, + missingTranslationHandler: { + provide: MissingTranslationHandler, + useFactory: missingTranslationHandler, + }, + }), + ], +}) +export class TranslationModule { + private translateService = inject(TranslateService); + private stateStorageService = inject(StateStorageService); + + constructor() { + this.translateService.setDefaultLang('pt-pt'); + // if user have changed language and navigates away from the application and back to the application then use previously choosed language + const langKey = this.stateStorageService.getLocale() ?? 'pt-pt'; + this.translateService.use(langKey); + } +} diff --git a/src/main/webapp/app/shared/pagination/index.ts b/src/main/webapp/app/shared/pagination/index.ts new file mode 100644 index 0000000..395ed88 --- /dev/null +++ b/src/main/webapp/app/shared/pagination/index.ts @@ -0,0 +1 @@ +export { default as ItemCountComponent } from './item-count.component'; diff --git a/src/main/webapp/app/shared/pagination/item-count.component.spec.ts b/src/main/webapp/app/shared/pagination/item-count.component.spec.ts new file mode 100644 index 0000000..9e91b1d --- /dev/null +++ b/src/main/webapp/app/shared/pagination/item-count.component.spec.ts @@ -0,0 +1,67 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import TranslateDirective from 'app/shared/language/translate.directive'; + +import ItemCountComponent from './item-count.component'; + +describe('ItemCountComponent test', () => { + let comp: ItemCountComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ItemCountComponent, TranslateModule.forRoot(), TranslateDirective], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemCountComponent); + comp = fixture.componentInstance; + }); + + describe('UI logic tests', () => { + it('should initialize with undefined', () => { + expect(comp.first).toBeUndefined(); + expect(comp.second).toBeUndefined(); + expect(comp.total).toBeUndefined(); + }); + + it('should set calculated numbers to undefined if the page value is not yet defined', () => { + // GIVEN + comp.params = { page: undefined, totalItems: 0, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBeUndefined(); + expect(comp.second).toBeUndefined(); + }); + + it('should change the content on page change', () => { + // GIVEN + comp.params = { page: 1, totalItems: 100, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBe(1); + expect(comp.second).toBe(10); + expect(comp.total).toBe(100); + + // GIVEN + comp.params = { page: 2, totalItems: 100, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBe(11); + expect(comp.second).toBe(20); + expect(comp.total).toBe(100); + }); + + it('should set the second number to totalItems if this is the last page which contains less than itemsPerPage items', () => { + // GIVEN + comp.params = { page: 2, totalItems: 16, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBe(11); + expect(comp.second).toBe(16); + expect(comp.total).toBe(16); + }); + }); +}); diff --git a/src/main/webapp/app/shared/pagination/item-count.component.ts b/src/main/webapp/app/shared/pagination/item-count.component.ts new file mode 100644 index 0000000..0ac3521 --- /dev/null +++ b/src/main/webapp/app/shared/pagination/item-count.component.ts @@ -0,0 +1,34 @@ +import { Component, Input } from '@angular/core'; +import TranslateDirective from '../language/translate.directive'; + +/** + * A component that will take care of item count statistics of a pagination. + */ +@Component({ + standalone: true, + selector: 'jhi-item-count', + template: `
    `, + imports: [TranslateDirective], +}) +export default class ItemCountComponent { + /** + * @param params Contains parameters for component: + * page Current page number + * totalItems Total number of items + * itemsPerPage Number of items per page + */ + @Input() set params(params: { page?: number; totalItems?: number; itemsPerPage?: number }) { + if (params.page && params.totalItems !== undefined && params.itemsPerPage) { + this.first = (params.page - 1) * params.itemsPerPage + 1; + this.second = params.page * params.itemsPerPage < params.totalItems ? params.page * params.itemsPerPage : params.totalItems; + } else { + this.first = undefined; + this.second = undefined; + } + this.total = params.totalItems; + } + + first?: number; + second?: number; + total?: number; +} diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts new file mode 100644 index 0000000..6511f7b --- /dev/null +++ b/src/main/webapp/app/shared/shared.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TranslateModule } from '@ngx-translate/core'; + +import FindLanguageFromKeyPipe from './language/find-language-from-key.pipe'; +import TranslateDirective from './language/translate.directive'; +import { AlertComponent } from './alert/alert.component'; +import { AlertErrorComponent } from './alert/alert-error.component'; + +/** + * Application wide Module + */ +@NgModule({ + imports: [AlertComponent, AlertErrorComponent, FindLanguageFromKeyPipe, TranslateDirective], + exports: [ + CommonModule, + NgbModule, + FontAwesomeModule, + AlertComponent, + AlertErrorComponent, + TranslateModule, + FindLanguageFromKeyPipe, + TranslateDirective, + ], +}) +export default class SharedModule {} diff --git a/src/main/webapp/app/shared/sort/index.ts b/src/main/webapp/app/shared/sort/index.ts new file mode 100644 index 0000000..6e7fd41 --- /dev/null +++ b/src/main/webapp/app/shared/sort/index.ts @@ -0,0 +1,4 @@ +export * from './sort-by.directive'; +export * from './sort-state'; +export * from './sort.directive'; +export * from './sort.service'; diff --git a/src/main/webapp/app/shared/sort/sort-by.directive.spec.ts b/src/main/webapp/app/shared/sort/sort-by.directive.spec.ts new file mode 100644 index 0000000..e70f073 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-by.directive.spec.ts @@ -0,0 +1,121 @@ +import { Component, DebugElement, inject } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { FaIconComponent, FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import { fas, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; + +import { SortByDirective } from './sort-by.directive'; +import { SortDirective } from './sort.directive'; +import { sortStateSignal } from './sort-state'; + +@Component({ + standalone: true, + imports: [SortDirective, SortByDirective, FaIconComponent], + template: ` + + + + + + +
    + ID + +
    + `, +}) +class TestSortByDirectiveComponent { + sortState = sortStateSignal({ predicate: 'name' }); + sortAllowed = true; + transition = jest.fn(); + + private library = inject(FaIconLibrary); + + constructor() { + this.library.addIconPacks(fas); + this.library.addIcons(faSort, faSortDown, faSortUp); + } +} + +describe('Directive: SortByDirective', () => { + let component: TestSortByDirectiveComponent; + let fixture: ComponentFixture; + let tableHead: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestSortByDirectiveComponent], + }).compileComponents(); + fixture = TestBed.createComponent(TestSortByDirectiveComponent); + component = fixture.componentInstance; + tableHead = fixture.debugElement.query(By.directive(SortByDirective)); + }); + + it('should have a neutral state for predicate column and undefined order value', () => { + // GIVEN + component.sortState.set({ predicate: 'name' }); + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(sortByDirective.jhiSortBy).toEqual('name'); + expect(sortByDirective.iconComponent?.icon).toEqual(faSort.iconName); + }); + + it('should have an asc state for predicate column and true asc value', () => { + // GIVEN + component.sortState.set({ predicate: 'name', order: 'asc' }); + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(sortByDirective.jhiSortBy).toEqual('name'); + expect(sortByDirective.iconComponent?.icon).toEqual(faSortUp.iconName); + }); + + it('should have a desc state for predicate column and desc value', () => { + // GIVEN + component.sortState.set({ predicate: 'name', order: 'desc' }); + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(sortByDirective.jhiSortBy).toEqual('name'); + expect(sortByDirective.iconComponent?.icon).toEqual(faSortDown.iconName); + }); + + it('should have a neutral state for non-predicate column', () => { + // GIVEN + component.sortState.set({ predicate: 'non-existing-column', order: 'asc' }); + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(sortByDirective.jhiSortBy).toEqual('name'); + expect(sortByDirective.iconComponent?.icon).toEqual(faSort.iconName); + }); + + it('multiple clicks at same component, should call SortDirective sort', () => { + // GIVEN + const sortDirective = tableHead.injector.get(SortDirective); + sortDirective.sort = jest.fn(); + + // WHEN + fixture.detectChanges(); + tableHead.triggerEventHandler('click', null); + tableHead.triggerEventHandler('click', null); + + // THEN + expect(sortDirective.sort).toHaveBeenCalledTimes(2); + expect(sortDirective.sort).toHaveBeenNthCalledWith(1, 'name'); + expect(sortDirective.sort).toHaveBeenNthCalledWith(2, 'name'); + }); +}); diff --git a/src/main/webapp/app/shared/sort/sort-by.directive.ts b/src/main/webapp/app/shared/sort/sort-by.directive.ts new file mode 100644 index 0000000..82fc280 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-by.directive.ts @@ -0,0 +1,41 @@ +import { ContentChild, Directive, Host, HostListener, Input, effect } from '@angular/core'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { faSort, faSortDown, faSortUp, IconDefinition } from '@fortawesome/free-solid-svg-icons'; + +import { SortDirective } from './sort.directive'; + +@Directive({ + standalone: true, + selector: '[jhiSortBy]', +}) +export class SortByDirective { + @Input() jhiSortBy!: string; + + @ContentChild(FaIconComponent, { static: false }) + iconComponent?: FaIconComponent; + + protected sortIcon = faSort; + protected sortAscIcon = faSortUp; + protected sortDescIcon = faSortDown; + + constructor(@Host() private sort: SortDirective) { + effect(() => { + if (this.iconComponent) { + let icon: IconDefinition = this.sortIcon; + const { predicate, order } = this.sort.sortState(); + if (predicate === this.jhiSortBy && order !== undefined) { + icon = order === 'asc' ? this.sortAscIcon : this.sortDescIcon; + } + this.iconComponent.icon = icon.iconName; + this.iconComponent.render(); + } + }); + } + + @HostListener('click') + onClick(): void { + if (this.iconComponent) { + this.sort.sort(this.jhiSortBy); + } + } +} diff --git a/src/main/webapp/app/shared/sort/sort-state.ts b/src/main/webapp/app/shared/sort/sort-state.ts new file mode 100644 index 0000000..fe667b4 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-state.ts @@ -0,0 +1,12 @@ +import { signal, Signal, WritableSignal } from '@angular/core'; + +export type SortOrder = 'asc' | 'desc'; + +export type SortState = { predicate?: string; order?: SortOrder }; + +export type SortStateSignal = Signal; + +export const sortStateSignal = (state: SortState): WritableSignal => + signal(state, { + equal: (a, b) => a.predicate === b.predicate && a.order === b.order, + }); diff --git a/src/main/webapp/app/shared/sort/sort.directive.spec.ts b/src/main/webapp/app/shared/sort/sort.directive.spec.ts new file mode 100644 index 0000000..eac9ac7 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort.directive.spec.ts @@ -0,0 +1,87 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { SortDirective } from './sort.directive'; +import { SortState, sortStateSignal } from './sort-state'; + +@Component({ + standalone: true, + imports: [SortDirective], + template: ` + + + + +
    + `, +}) +class TestSortDirectiveComponent { + sortState = sortStateSignal({ predicate: 'ID' }); + transition = jest.fn().mockImplementation((sortState: SortState) => { + this.sortState.set(sortState); + }); +} + +describe('Directive: SortDirective', () => { + let component: TestSortDirectiveComponent; + let fixture: ComponentFixture; + let tableRow: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestSortDirectiveComponent], + }); + fixture = TestBed.createComponent(TestSortDirectiveComponent); + component = fixture.componentInstance; + tableRow = fixture.debugElement.query(By.directive(SortDirective)); + }); + + it('should invoke sortChange function', () => { + // GIVEN + const sortDirective = tableRow.injector.get(SortDirective); + + // WHEN + fixture.detectChanges(); + sortDirective.sort('ID'); + + // THEN + expect(component.transition).toHaveBeenCalledTimes(1); + expect(component.transition).toHaveBeenCalledWith({ predicate: 'ID', order: 'asc' }); + }); + + it('should change sort order to descending, neutral when same field is sorted again', () => { + // GIVEN + const sortDirective = tableRow.injector.get(SortDirective); + + // WHEN + fixture.detectChanges(); + sortDirective.sort('ID'); + // sort again + sortDirective.sort('ID'); + // sort again + sortDirective.sort('ID'); + + // THEN + expect(component.transition).toHaveBeenCalledTimes(3); + expect(component.transition).toHaveBeenNthCalledWith(1, { predicate: 'ID', order: 'asc' }); + expect(component.transition).toHaveBeenNthCalledWith(2, { predicate: 'ID', order: 'desc' }); + expect(component.transition).toHaveBeenNthCalledWith(3, { predicate: 'ID', order: 'asc' }); + }); + + it('should change sort order to ascending when different field is sorted', () => { + // GIVEN + const sortDirective = tableRow.injector.get(SortDirective); + + // WHEN + fixture.detectChanges(); + sortDirective.sort('ID'); + // sort again + sortDirective.sort('NAME'); + + // THEN + expect(component.transition).toHaveBeenCalledTimes(2); + expect(component.transition).toHaveBeenNthCalledWith(1, { predicate: 'ID', order: 'asc' }); + expect(component.transition).toHaveBeenNthCalledWith(2, { predicate: 'NAME', order: 'asc' }); + }); +}); diff --git a/src/main/webapp/app/shared/sort/sort.directive.ts b/src/main/webapp/app/shared/sort/sort.directive.ts new file mode 100644 index 0000000..39e7b21 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort.directive.ts @@ -0,0 +1,24 @@ +import { Directive, EventEmitter, Input, Output } from '@angular/core'; +import { SortOrder, SortState, SortStateSignal } from './sort-state'; + +export interface SortChangeDirective { + sortChange: EventEmitter; + + sort(field: T): void; +} + +@Directive({ + standalone: true, + selector: '[jhiSort]', +}) +export class SortDirective implements SortChangeDirective { + @Input() sortState!: SortStateSignal; + + @Output() sortChange = new EventEmitter(); + + sort(field: string): void { + const { predicate, order } = this.sortState(); + const toggle = (): SortOrder => (order === 'asc' ? 'desc' : 'asc'); + this.sortChange.emit({ predicate: field, order: field !== predicate ? 'asc' : toggle() }); + } +} diff --git a/src/main/webapp/app/shared/sort/sort.service.spec.ts b/src/main/webapp/app/shared/sort/sort.service.spec.ts new file mode 100644 index 0000000..4730e48 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort.service.spec.ts @@ -0,0 +1,46 @@ +import { SortService } from './sort.service'; + +describe('sort state', () => { + const service = new SortService(); + + describe('parseSortParam', () => { + it('should accept undefined value', () => { + const sortState = service.parseSortParam(undefined); + expect(sortState).toEqual({}); + }); + it('should accept empty string', () => { + const sortState = service.parseSortParam(''); + expect(sortState).toEqual({}); + }); + it('should accept predicate only string', () => { + const sortState = service.parseSortParam('predicate'); + expect(sortState).toEqual({ predicate: 'predicate' }); + }); + it('should accept predicate and ASC string', () => { + const sortState = service.parseSortParam('predicate,asc'); + expect(sortState).toEqual({ predicate: 'predicate', order: 'asc' }); + }); + it('should accept predicate and DESC string', () => { + const sortState = service.parseSortParam('predicate,desc'); + expect(sortState).toEqual({ predicate: 'predicate', order: 'desc' }); + }); + }); + describe('buildSortParam', () => { + it('should accept empty object', () => { + const sortParam = service.buildSortParam({}); + expect(sortParam).toEqual([]); + }); + it('should accept object with predicate', () => { + const sortParam = service.buildSortParam({ predicate: 'column' }); + expect(sortParam).toEqual([]); + }); + it('should accept object with predicate and asc value', () => { + const sortParam = service.buildSortParam({ predicate: 'column', order: 'asc' }); + expect(sortParam).toEqual(['column,asc']); + }); + it('should accept object with predicate and desc value', () => { + const sortParam = service.buildSortParam({ predicate: 'column', order: 'desc' }); + expect(sortParam).toEqual(['column,desc']); + }); + }); +}); diff --git a/src/main/webapp/app/shared/sort/sort.service.ts b/src/main/webapp/app/shared/sort/sort.service.ts new file mode 100644 index 0000000..e05e208 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { SortState } from './sort-state'; + +@Injectable({ providedIn: 'root' }) +export class SortService { + private collator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', + }); + + public startSort({ predicate, order }: Required, fallback?: Required): (a: any, b: any) => number { + const multiply = order === 'desc' ? -1 : 1; + return (a: any, b: any) => { + const compare = this.collator.compare(a[predicate], b[predicate]); + if (compare === 0 && fallback) { + return this.startSort(fallback)(a, b); + } + return compare * multiply; + }; + } + + public parseSortParam(sortParam: string | undefined): SortState { + if (sortParam?.includes(',')) { + const split = sortParam.split(','); + if (split[0]) { + return { predicate: split[0], order: split[1] as any }; + } + } + return { predicate: sortParam?.length ? sortParam : undefined }; + } + + public buildSortParam({ predicate, order }: SortState, fallback?: string): string[] { + const sortParam = predicate && order ? [`${predicate},${order}`] : []; + if (fallback && predicate !== fallback) { + sortParam.push(`${fallback},asc`); + } + return sortParam; + } +} diff --git a/src/main/webapp/app/shared/utils/format-helper.ts b/src/main/webapp/app/shared/utils/format-helper.ts new file mode 100644 index 0000000..7f25df7 --- /dev/null +++ b/src/main/webapp/app/shared/utils/format-helper.ts @@ -0,0 +1,36 @@ +export class FormatHelper { + + // Format decimal + static formatDecimalValue(value: number): string { + if (value === null || value === undefined) { + return ''; // Handle null/undefined case + } + + // >=0 && <1 + if (value < 1) { + return new Intl.NumberFormat('fr-FR', { + useGrouping: true, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(parseFloat(value.toFixed(2))); + } + + // >=10 + if (value>=10) { + return new Intl.NumberFormat('fr-FR', { + useGrouping: true, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(parseFloat(value.toFixed(0))); + } + + // >=1 && <10 + // this will format 1234567 as 1 234 567 + return new Intl.NumberFormat('fr-FR', { + useGrouping: true, + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }).format(parseFloat(value.toFixed(1))); + } +} + \ No newline at end of file diff --git a/src/main/webapp/bootstrap.ts b/src/main/webapp/bootstrap.ts new file mode 100644 index 0000000..dd28f62 --- /dev/null +++ b/src/main/webapp/bootstrap.ts @@ -0,0 +1,16 @@ +import { enableProdMode } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import AppComponent from './app/app.component'; + +import { DEBUG_INFO_ENABLED } from './app/app.constants'; + +// disable debug data on prod profile to improve performance +if (!DEBUG_INFO_ENABLED) { + enableProdMode(); +} + +bootstrapApplication(AppComponent, appConfig) + // eslint-disable-next-line no-console + .then(() => console.log('Application started')) + .catch(err => console.error(err)); diff --git a/src/main/webapp/content/css/loading.css b/src/main/webapp/content/css/loading.css new file mode 100644 index 0000000..678e7b6 --- /dev/null +++ b/src/main/webapp/content/css/loading.css @@ -0,0 +1,152 @@ +@keyframes lds-pacman-1 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + } + 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@-webkit-keyframes lds-pacman-1 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + } + 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@keyframes lds-pacman-2 { + 0% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + 50% { + -webkit-transform: rotate(225deg); + transform: rotate(225deg); + } + 100% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } +} +@-webkit-keyframes lds-pacman-2 { + 0% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + 50% { + -webkit-transform: rotate(225deg); + transform: rotate(225deg); + } + 100% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } +} +@keyframes lds-pacman-3 { + 0% { + -webkit-transform: translate(190px, 0); + transform: translate(190px, 0); + opacity: 0; + } + 20% { + opacity: 1; + } + 100% { + -webkit-transform: translate(70px, 0); + transform: translate(70px, 0); + opacity: 1; + } +} +@-webkit-keyframes lds-pacman-3 { + 0% { + -webkit-transform: translate(190px, 0); + transform: translate(190px, 0); + opacity: 0; + } + 20% { + opacity: 1; + } + 100% { + -webkit-transform: translate(70px, 0); + transform: translate(70px, 0); + opacity: 1; + } +} + +.app-loading { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + top: 10em; +} +.app-loading p { + display: block; + font-size: 1.17em; + margin-inline-start: 0px; + margin-inline-end: 0px; + font-weight: normal; +} + +.app-loading .lds-pacman { + position: relative; + margin: auto; + width: 200px !important; + height: 200px !important; + -webkit-transform: translate(-100px, -100px) scale(1) translate(100px, 100px); + transform: translate(-100px, -100px) scale(1) translate(100px, 100px); +} +.app-loading .lds-pacman > div:nth-child(2) div { + position: absolute; + top: 40px; + left: 40px; + width: 120px; + height: 60px; + border-radius: 120px 120px 0 0; + background: #bbcedd; + -webkit-animation: lds-pacman-1 1s linear infinite; + animation: lds-pacman-1 1s linear infinite; + -webkit-transform-origin: 60px 60px; + transform-origin: 60px 60px; +} +.app-loading .lds-pacman > div:nth-child(2) div:nth-child(2) { + -webkit-animation: lds-pacman-2 1s linear infinite; + animation: lds-pacman-2 1s linear infinite; +} +.app-loading .lds-pacman > div:nth-child(1) div { + position: absolute; + top: 97px; + left: -8px; + width: 24px; + height: 10px; + background-image: url('/content/images/logo-jhipster.png'); + background-size: contain; + -webkit-animation: lds-pacman-3 1s linear infinite; + animation: lds-pacman-3 1.5s linear infinite; +} +.app-loading .lds-pacman > div:nth-child(1) div:nth-child(1) { + -webkit-animation-delay: -0.67s; + animation-delay: -1s; +} +.app-loading .lds-pacman > div:nth-child(1) div:nth-child(2) { + -webkit-animation-delay: -0.33s; + animation-delay: -0.5s; +} +.app-loading .lds-pacman > div:nth-child(1) div:nth-child(3) { + -webkit-animation-delay: 0s; + animation-delay: 0s; +} diff --git a/src/main/webapp/content/images/RouteZero.png b/src/main/webapp/content/images/RouteZero.png new file mode 100644 index 0000000..a1d7cc3 Binary files /dev/null and b/src/main/webapp/content/images/RouteZero.png differ diff --git a/src/main/webapp/content/images/chart_emissoes.png b/src/main/webapp/content/images/chart_emissoes.png new file mode 100644 index 0000000..064a318 Binary files /dev/null and b/src/main/webapp/content/images/chart_emissoes.png differ diff --git a/src/main/webapp/content/images/grafico1.png b/src/main/webapp/content/images/grafico1.png new file mode 100644 index 0000000..a28cac6 Binary files /dev/null and b/src/main/webapp/content/images/grafico1.png differ diff --git a/src/main/webapp/content/images/grafico2.png b/src/main/webapp/content/images/grafico2.png new file mode 100644 index 0000000..4d6eba5 Binary files /dev/null and b/src/main/webapp/content/images/grafico2.png differ diff --git a/src/main/webapp/content/images/grafico3.png b/src/main/webapp/content/images/grafico3.png new file mode 100644 index 0000000..7e8277a Binary files /dev/null and b/src/main/webapp/content/images/grafico3.png differ diff --git a/src/main/webapp/content/images/graphic-emissoes-globais.png b/src/main/webapp/content/images/graphic-emissoes-globais.png new file mode 100644 index 0000000..643145f Binary files /dev/null and b/src/main/webapp/content/images/graphic-emissoes-globais.png differ diff --git a/src/main/webapp/content/images/graphic-gases.png b/src/main/webapp/content/images/graphic-gases.png new file mode 100644 index 0000000..e3fc441 Binary files /dev/null and b/src/main/webapp/content/images/graphic-gases.png differ diff --git a/src/main/webapp/content/images/graphic1.png b/src/main/webapp/content/images/graphic1.png new file mode 100644 index 0000000..ef29d5a Binary files /dev/null and b/src/main/webapp/content/images/graphic1.png differ diff --git a/src/main/webapp/content/images/graphic2.png b/src/main/webapp/content/images/graphic2.png new file mode 100644 index 0000000..fe03b89 Binary files /dev/null and b/src/main/webapp/content/images/graphic2.png differ diff --git a/src/main/webapp/content/images/graphic3.png b/src/main/webapp/content/images/graphic3.png new file mode 100644 index 0000000..190aa5d Binary files /dev/null and b/src/main/webapp/content/images/graphic3.png differ diff --git a/src/main/webapp/content/images/graphic4.png b/src/main/webapp/content/images/graphic4.png new file mode 100644 index 0000000..1f5c3ae Binary files /dev/null and b/src/main/webapp/content/images/graphic4.png differ diff --git a/src/main/webapp/content/images/home_banner.jpg b/src/main/webapp/content/images/home_banner.jpg new file mode 100644 index 0000000..67405c2 Binary files /dev/null and b/src/main/webapp/content/images/home_banner.jpg differ diff --git a/src/main/webapp/content/images/inNova_logo.png b/src/main/webapp/content/images/inNova_logo.png new file mode 100644 index 0000000..cb244f8 Binary files /dev/null and b/src/main/webapp/content/images/inNova_logo.png differ diff --git a/src/main/webapp/content/images/logo-jhipster.png b/src/main/webapp/content/images/logo-jhipster.png new file mode 100644 index 0000000..6a005bf Binary files /dev/null and b/src/main/webapp/content/images/logo-jhipster.png differ diff --git a/src/main/webapp/content/images/logo-nova-group-zero.png b/src/main/webapp/content/images/logo-nova-group-zero.png new file mode 100644 index 0000000..ff6f499 Binary files /dev/null and b/src/main/webapp/content/images/logo-nova-group-zero.png differ diff --git a/src/main/webapp/content/images/logo-nova-group-zero.svg b/src/main/webapp/content/images/logo-nova-group-zero.svg new file mode 100644 index 0000000..e551dbc --- /dev/null +++ b/src/main/webapp/content/images/logo-nova-group-zero.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/content/images/table_emissoes.png b/src/main/webapp/content/images/table_emissoes.png new file mode 100644 index 0000000..9a0bd31 Binary files /dev/null and b/src/main/webapp/content/images/table_emissoes.png differ diff --git a/src/main/webapp/content/scss/_bootstrap-variables.scss b/src/main/webapp/content/scss/_bootstrap-variables.scss new file mode 100644 index 0000000..fb0b6f9 --- /dev/null +++ b/src/main/webapp/content/scss/_bootstrap-variables.scss @@ -0,0 +1,36 @@ +/* + * Bootstrap overrides https://getbootstrap.com/docs/5.1/customize/sass/ + * All values defined in bootstrap source + * https://github.com/twbs/bootstrap/blob/v5.1.3/scss/_variables.scss can be overwritten here + * Make sure not to add !default to values here + */ + +// Options: +// Quickly modify global styling by enabling or disabling optional features. +$enable-rounded: true; +$enable-shadows: false; +$enable-gradients: false; +$enable-transitions: true; +$enable-hover-media-query: false; +$enable-grid-classes: true; +$enable-print-styles: true; + +// Components: +// Define common padding and border radius sizes and more. + +$border-radius: 0.15rem; +$border-radius-lg: 0.125rem; +$border-radius-sm: 0.1rem; + +// Body: +// Settings for the `` element. + +$body-bg: #ffffff; + +// Typography: +// Font, line-height, and color for body text, headings, and more. + +$font-size-base: 1rem; + +$dropdown-link-hover-color: white; +$dropdown-link-hover-bg: #343a40; diff --git a/src/main/webapp/content/scss/global.scss b/src/main/webapp/content/scss/global.scss new file mode 100644 index 0000000..89e03d9 --- /dev/null +++ b/src/main/webapp/content/scss/global.scss @@ -0,0 +1,626 @@ +@import 'bootstrap-variables'; +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; + +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); + +/* ============================================================== +Bootstrap tweaks +===============================================================*/ +body.navbar-present { + padding-top: 75px; +} + +body, +h1, +h2, +h3, +h4 { + font-family: "Poppins", sans-serif; + font-weight: 300; +} +.display-4{ + color: #7F937F; + font-weight: 700; +} +.title{ + font-weight: 700; +} +#page-heading{ + color: #7F937F; + font-weight: 700; +} + +/* Increase contrast of links to get 100% on Lighthouse Accessibility Audit. Override this color if you want to change the link color, or use a Bootswatch theme */ +a { + color: #348141; + font-weight: bold; +} + +a:hover { + color: #7F937F; +} + +.text-primary{ + color: #348141 !important; +} +.bg-primary{ + background-color: #348141 !important; + color: #FFF; +} + +.btn{ + border-radius: 20px; + font-weight: 500; + margin-bottom: 7px; +} + +.btn-primary{ + background-color: #348141 !important; + border: none; + &:hover { + background-color: #7F937F; + } + &:active { + background-color: #7F937F !important; + } + &:disabled{ + background-color: #7F937F; + opacity: .8; + } + &.show{ + background-color: #7F937F; + } +} + +.btn-info{ + background-color: #444 !important; + border: none; + color: #FFF !important; + &:hover{ + background-color: #777 !important; + } + &:active{ + background-color: #777 !important; + } +} +.btn-danger{ + background-color: #FF7470; + border: none; + color: #FFF; + &:hover{ + background-color: red; + } +} +.jh-export-entity{ + margin-left: 10px; + padding: 12px 25px; +} + +.header{ + min-height: 360px; + border-radius: 30px; + background: unquote("url('/content/images/home_banner.jpg')") #348141 right bottom no-repeat; + background-size: cover; +} + +.mat-tree-node{ + min-height: 50px !important; + .tree-node-title{ + font-family: "Poppins", sans-serif; + font-size: 16px; + } +} +organization-tree-actions{ + margin-top: 10px; +} + +.org-type-icon{ + max-height: 21px !important; + margin-right: 10px !important; +} + +.tree-btn-add{ + background: transparent !important; + color: #348141 !important; + border-radius: 15px !important; + &:hover{ + background: #348141 !important; + color: #FFF !important; + } +} +.tree-btn-detail{ + background: transparent !important; + color: #444 !important; + border-radius: 15px; + &:hover{ + background: #444; + color: #FFF !important; + } +} +.tree-btn-edit{ + background: transparent !important; + color: #348141; + &:hover{ + background: #348141 !important; + color: #FFF !important; + } +} +.tree-btn-delete{ + background: transparent; + color: #FF7470; + border-radius: 15px !important; + &:hover{ + background: #FF7470; + color: #FFF; + } +} + +.dropdown-item{ + color: #CCC; +} +.alert{ + border-radius: 15px; + text-align: left; +} +.alert-danger{ + background-color: rgba(255,116,112,.5); + border: none; +} +.jh-create-entity, .me-2 { + padding: 12px 25px; +} + +.text-end{ + .btn{ + margin-right: 5px; + } + .btn-primary{ + background-color: transparent !important; + color: #348141; + border-radius: 15px !important; + &:hover{ + background-color: #348141; + color: #FFF; + } + } + .btn-info{ + background-color: transparent !important; + color: #222 !important; + border-radius: 15px !important; + &:hover{ + background-color: #222 !important; + color: #FFF !important; + } + } + .btn-danger{ + background-color: transparent; + color: #FF7470; + border-radius: 15px !important; + &:hover{ + background-color: #FF7470; + color: #FFF; + } + } +} + +.table > :not(caption) > * > * { + vertical-align: middle; + padding: 12px; + .btn { + margin-bottom: 0; + } +} + + +#home-header{ + width: 100%; +} + +/* override hover color for dropdown-item forced by bootstrap to all a:not([href]):not([tabindex]) elements in _reboot.scss */ +a:not([href]):not([tabindex]):hover.dropdown-item { + color: $dropdown-link-hover-color; +} + +/* override .dropdown-item.active background-color on hover */ +.dropdown-item.active:hover { + background-color: mix($dropdown-link-hover-bg, $dropdown-link-active-bg, 50%); +} + +a:hover { + /* make sure browsers use the pointer cursor for anchors, even with no href */ + cursor: pointer; +} + +.dropdown-item:hover { + color: $dropdown-link-hover-color; +} + +.input-group{ + .btn-secondary{ + margin-bottom: 0; + } +} + +.ngb-dp-month{ + background: #FFF; +} + +.drag-handle{ + &:hover{ + cursor: grab; + } + &:active{ + cursor: grabbing; + } +} + +/* ========================================================================== +Browser Upgrade Prompt +========================================================================== */ +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +/* ========================================================================== +Generic styles +========================================================================== */ + +/* Error highlight on input fields */ +.ng-valid[required], +.ng-valid.required { + border-left: 5px solid green; +} + +.ng-invalid:not(form) { + border-left: 5px solid red; +} + +/* other generic styles */ + +.jh-card { + padding: 1.5%; + margin-top: 20px; + border: none; +} + +.error { + color: white; + background-color: red; +} + +.pad { + padding: 10px; +} + +.w-40 { + width: 40% !important; +} + +.w-60 { + width: 60% !important; +} + +.break { + white-space: normal; + word-break: break-all; +} +app-period-selector{ + margin-bottom: 30px; + display: block; + .form-control{ + width: auto; + } +} + +.form-label{ + padding-top: calc(0.375rem); + margin-bottom: 2px; + color: #999 !important; + font-size: 13px; + font-weight: 600; +} +.form-control { + appearance: auto; + padding: 10px 15px; + border-radius: 15px; + background-color: #eee; + color: #000; + font-size: 16px; + &:focus{ + border: 1px solid #999; + } +} + +.readonly { + background-color: #fff; + color: #000; + opacity: 1; + border: none; + padding: 10px 0px; + font-size: 18px; + &:focus{ + background-color: #fff; + color: #000; + } +} + +.footer { + padding: 10px 0; + border-top: 1px solid rgba(0, 0, 0, 0.125); + font-size: 13px; +} + +.hand, +[jhisortby] { + cursor: pointer; +} + +/* ========================================================================== +Custom alerts for notification +========================================================================== */ +.alerts { + .alert { + text-overflow: ellipsis; + pre { + background: none; + border: none; + font: inherit; + color: inherit; + padding: 0; + margin: 0; + } + .popover pre { + font-size: 10px; + } + } + .jhi-toast { + position: fixed; + width: 100%; + &.left { + left: 5px; + } + &.right { + right: 5px; + } + &.top { + top: 55px; + } + &.bottom { + bottom: 55px; + } + } +} + +@media screen and (min-width: 480px) { + .alerts .jhi-toast { + width: 50%; + } +} + +/* ========================================================================== +entity list page css +========================================================================== */ + +.table-entities thead th .d-flex > * { + margin: auto 0; +} + +/* ========================================================================== +entity detail page css +========================================================================== */ +.row-md.jh-entity-details { + display: grid; + grid-template-columns: auto 1fr; + column-gap: 10px; + line-height: 1.5; +} + +@media screen and (min-width: 768px) { + .row-md.jh-entity-details > { + dt { + float: left; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0.5em 0; + } + dd { + border-bottom: 1px solid #eee; + padding: 0.5em 0; + margin-left: 0; + } + } +} + +/* ========================================================================== +ui bootstrap tweaks +========================================================================== */ +.nav, +.pagination, +.carousel, +.panel-title a { + cursor: pointer; +} + +.thread-dump-modal-lock { + max-width: 450px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dropdown-menu { + padding-left: 0px; + background: rgba(0, 0, 0, .9); +} + +/* ========================================================================== +angular-cli removes postcss-rtl processed inline css, processed rules must be added here instead +========================================================================== */ +/* page-ribbon.component.scss */ +.ribbon { + left: -3.5em; + -moz-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -o-transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +/* navbar.component.scss */ +.navbar { + ul.navbar-nav { + .nav-item { + margin-left: 0.5em; + color: #FFF; + } + } +} +.navbar { + ul.navbar-nav { + .nav-item a{ + color: #FFF; + font-size: 14px; + } + } +} +.navbar { + ul.navbar-nav { + .nav-item a{ + fa-icon{ + color: #7F937F; + margin-right: 5px; + } + } + } +} +.navbar{ + .form-select-sm{ + height: 40px; + max-width: 150px; + padding: 10px; + margin-top: 0; + border: none; + border-radius: 15px; + background-color: #7F937F; + color: #FFF; + } +} +/* jhipster-needle-scss-add-main JHipster will add new css style */ + +.nested-table.table > :not(caption) > * > * { + padding: 0px; +} + +dashboard-component-accordion-table{ + display: block; + margin-bottom: 50px; + h4{ + color: rgb(127, 147, 127); + font-weight: 700; + } +} + +// Accordeon +.mat-accordion{ + .mat-expansion-panel{ + box-shadow: none !important; + border-bottom: 2px solid #CCC; + border-radius: 0px; + .mat-expansion-panel-header{ + padding: 12px 3px 12px 0; + font-family: "Poppins", sans-serif !important; + font-size: 18px; + font-weight: 700; + &:hover { + background-color: #FFF !important; + color: #348141; + } + .mat-content{ + } + .mat-expansion-indicator{ + &:after { + border-width: 0px 4px 4px 0 !important; + padding: 5px !important; + } + } + } + .mat-expansion-panel-content{ + font-weight: 400 !important; + .mat-expansion-panel-body{ + .ng-star-inserted{ + .mat-accordion{ + .mat-expansion-panel{ + .mat-expansion-panel-header{ + padding: 7px 2px; + .mat-expansion-indicator{ + &:after { + border-width: 0px 3px 3px 0 !important; + padding: 4px !important; + } + } + .mat-content{ + font-size: 14px; + font-weight: 600; + .label{ + padding-left: 15px; + } + .values{ + } + } + } + .mat-expansion-panel-content{ + .mat-expansion-panel-body{ + .ng-star-inserted{ + .mat-accordion{ + .mat-expansion-panel{ + .mat-expansion-panel-header{ + padding: 5px 2px; + .mat-content{ + font-size: 12px; + font-weight: 400; + .label{ + padding-left: 30px; + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} +.mat-expansion-panel-header.mat-expanded { + color: #348141; + .mat-expansion-indicator{ + &:after { + color: #348141; + } + } +} +.values-main { + color: #348141; + font-weight: bold; +} + +.ng-dropdown-panel { + background-color: white; + min-width: 450px; + padding: 5px; +} + +.ng-option-marked { + background-color: blue; +} \ No newline at end of file diff --git a/src/main/webapp/content/scss/vendor.scss b/src/main/webapp/content/scss/vendor.scss new file mode 100644 index 0000000..acf2df2 --- /dev/null +++ b/src/main/webapp/content/scss/vendor.scss @@ -0,0 +1,12 @@ +/* after changing this file run 'npm run webapp:build' */ + +/*************************** +put Sass variables here: +eg $input-color: red; +****************************/ +// Override Bootstrap variables +@import 'bootstrap-variables'; +// Import Bootstrap source files from node_modules +@import 'bootstrap/scss/bootstrap'; + +/* jhipster-needle-scss-add-vendor JHipster will add new css style */ diff --git a/src/main/webapp/content/tinymce/icons/default/icons.min.js b/src/main/webapp/content/tinymce/icons/default/icons.min.js new file mode 100644 index 0000000..620f554 --- /dev/null +++ b/src/main/webapp/content/tinymce/icons/default/icons.min.js @@ -0,0 +1 @@ +tinymce.IconManager.add("default",{icons:{"accessibility-check":'',"accordion-toggle":'',accordion:'',"action-next":'',"action-prev":'',addtag:'',"ai-prompt":'',ai:'',"align-center":'',"align-justify":'',"align-left":'',"align-none":'',"align-right":'',"arrow-left":'',"arrow-right":'',bold:'',bookmark:'',"border-style":'',"border-width":'',brightness:'',browse:'',cancel:'',"cell-background-color":'',"cell-border-color":'',"change-case":'',"character-count":'',"checklist-rtl":'',checklist:'',checkmark:'',"chevron-down":'',"chevron-left":'',"chevron-right":'',"chevron-up":'',close:'',"code-sample":'',"color-levels":'',"color-picker":'',"color-swatch-remove-color":'',"color-swatch":'',"comment-add":'',comment:'',contrast:'',copy:'',crop:'',"cut-column":'',"cut-row":'',cut:'',"document-properties":'',drag:'',"duplicate-column":'',"duplicate-row":'',duplicate:'',"edit-block":'',"edit-image":'',"embed-page":'',embed:'',emoji:'',export:'',fill:'',"flip-horizontally":'',"flip-vertically":'',footnote:'',"format-code":'',"format-painter":'',format:'',fullscreen:'',gallery:'',gamma:'',help:'',"highlight-bg-color":'',home:'',"horizontal-rule":'',"image-options":'',image:'',indent:'',info:'',"insert-character":'',"insert-time":'',invert:'',italic:'',language:'',"line-height":'',line:'',link:'',"list-bull-circle":'',"list-bull-default":'',"list-bull-square":'',"list-num-default-rtl":'',"list-num-default":'',"list-num-lower-alpha-rtl":'',"list-num-lower-alpha":'',"list-num-lower-greek-rtl":'',"list-num-lower-greek":'',"list-num-lower-roman-rtl":'',"list-num-lower-roman":'',"list-num-upper-alpha-rtl":'',"list-num-upper-alpha":'',"list-num-upper-roman-rtl":'',"list-num-upper-roman":'',lock:'',ltr:'',"math-equation":'',mentions:'',minus:'',"more-drawer":'',"new-document":'',"new-tab":'',"non-breaking":'',notice:'',"ordered-list-rtl":'',"ordered-list":'',orientation:'',outdent:'',"export-pdf":'',"export-word":'',"import-word":'',"page-break":'',paragraph:'',"paste-column-after":'',"paste-column-before":'',"paste-row-after":'',"paste-row-before":'',"paste-text":'',paste:'',"permanent-pen":'',plus:'',preferences:'',preview:'',print:'',quote:'',redo:'',reload:'',"remove-formatting":'',remove:'',"resize-handle":'',resize:'',"restore-draft":'',"revision-history":'',"rotate-left":'',"rotate-right":'',rtl:'',save:'',search:'',"select-all":'',selected:'',send:'',settings:'',sharpen:'',sourcecode:'',"spell-check":'',"strike-through":'',subscript:'',superscript:'',"table-caption":'',"table-cell-classes":'',"table-cell-properties":'',"table-cell-select-all":'',"table-cell-select-inner":'',"table-classes":'',"table-delete-column":'',"table-delete-row":'',"table-delete-table":'',"table-insert-column-after":'',"table-insert-column-before":'',"table-insert-row-above":'',"table-insert-row-after":'',"table-left-header":'',"table-merge-cells":'',"table-row-numbering-rtl":'',"table-row-numbering":'',"table-row-properties":'',"table-split-cells":'',"table-top-header":'',table:'',"template-add":'',template:'',"temporary-placeholder":'',"text-color":'',"text-size-decrease":'',"text-size-increase":'',toc:'',translate:'',typography:'',underline:'',undo:'',unlink:'',unlock:'',"unordered-list":'',unselected:'',upload:'',"add-file":'',adjustments:'',"alt-text":'',"auto-image-enhancement":'',blur:'',box:'',camera:'',caption:'',dropbox:'',evernote:'',exposure:'',fb:'',flickr:'',folder:'',"google-drive":'',"google-photos":'',grayscale:'',huddle:'',"image-decorative":'',"image-enhancements":'',instagram:'',onedrive:'',"photo-filter":'',"revert-changes":'',saturation:'',"transform-image":'',vibrance:'',vk:'',warmth:'',user:'',"vertical-align":'',visualblocks:'',visualchars:'',warning:'',"zoom-in":'',"zoom-out":''}}); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/langs/README.md b/src/main/webapp/content/tinymce/langs/README.md new file mode 100644 index 0000000..cd93d8c --- /dev/null +++ b/src/main/webapp/content/tinymce/langs/README.md @@ -0,0 +1,3 @@ +This is where language files should be placed. + +Please DO NOT translate these directly, use this service instead: https://crowdin.com/project/tinymce diff --git a/src/main/webapp/content/tinymce/license.md b/src/main/webapp/content/tinymce/license.md new file mode 100644 index 0000000..70454a6 --- /dev/null +++ b/src/main/webapp/content/tinymce/license.md @@ -0,0 +1,6 @@ +# Software License Agreement + +**TinyMCE** – [](https://github.com/tinymce/tinymce) +Copyright (c) 2024, Ephox Corporation DBA Tiny Technologies, Inc. + +Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). diff --git a/src/main/webapp/content/tinymce/models/dom/model.min.js b/src/main/webapp/content/tinymce/models/dom/model.min.js new file mode 100644 index 0000000..bce8e62 --- /dev/null +++ b/src/main/webapp/content/tinymce/models/dom/model.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.ModelManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(o=n=e,(r=String).prototype.isPrototypeOf(o)||(null===(s=n.constructor)||void 0===s?void 0:s.name)===r.name)?"string":t;var o,n,r,s})(t)===e,o=e=>t=>typeof t===e,n=e=>t=>e===t,r=t("string"),s=t("object"),l=t("array"),a=n(null),c=o("boolean"),i=n(void 0),m=e=>!(e=>null==e)(e),d=o("function"),u=o("number"),f=()=>{},g=e=>()=>e,h=e=>e,p=(e,t)=>e===t;function b(e,...t){return(...o)=>{const n=t.concat(o);return e.apply(null,n)}}const w=e=>t=>!e(t),v=e=>e(),y=g(!1),x=g(!0);class C{constructor(e,t){this.tag=e,this.value=t}static some(e){return new C(!0,e)}static none(){return C.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?C.some(e(this.value)):C.none()}bind(e){return this.tag?e(this.value):C.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:C.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return m(e)?C.some(e):C.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}C.singletonNone=new C(!1);const S=Array.prototype.slice,T=Array.prototype.indexOf,R=Array.prototype.push,D=(e,t)=>{return o=e,n=t,T.call(o,n)>-1;var o,n},O=(e,t)=>{for(let o=0,n=e.length;o{const o=[];for(let n=0;n{const o=e.length,n=new Array(o);for(let r=0;r{for(let o=0,n=e.length;o{const o=[],n=[];for(let r=0,s=e.length;r{const o=[];for(let n=0,r=e.length;n(((e,t)=>{for(let o=e.length-1;o>=0;o--)t(e[o],o)})(e,((e,n)=>{o=t(o,e,n)})),o),A=(e,t,o)=>(N(e,((e,n)=>{o=t(o,e,n)})),o),L=(e,t)=>((e,t,o)=>{for(let n=0,r=e.length;n{for(let o=0,n=e.length;o{const t=[];for(let o=0,n=e.length;oM(E(e,t)),P=(e,t)=>{for(let o=0,n=e.length;o{const o={};for(let n=0,r=e.length;nt>=0&&tF(e,0),$=e=>F(e,e.length-1),V=(e,t)=>{for(let o=0;o{const o=q(e);for(let n=0,r=o.length;nY(e,((e,o)=>({k:o,v:t(e,o)}))),Y=(e,t)=>{const o={};return G(e,((e,n)=>{const r=t(e,n);o[r.k]=r.v})),o},J=(e,t)=>{const o=[];return G(e,((e,n)=>{o.push(t(e,n))})),o},Q=e=>J(e,h),X=(e,t)=>U.call(e,t),Z="undefined"!=typeof window?window:Function("return this;")(),ee=(e,t)=>((e,t)=>{let o=null!=t?t:Z;for(let t=0;t{const t=ee("ownerDocument.defaultView",e);return s(e)&&((e=>((e,t)=>{const o=((e,t)=>ee(e,t))(e,t);if(null==o)throw new Error(e+" not available on this browser");return o})("HTMLElement",e))(t).prototype.isPrototypeOf(e)||/^HTML\w*Element$/.test(te(e).constructor.name))},ne=e=>e.dom.nodeName.toLowerCase(),re=e=>e.dom.nodeType,se=e=>t=>re(t)===e,le=e=>8===re(e)||"#comment"===ne(e),ae=e=>ce(e)&&oe(e.dom),ce=se(1),ie=se(3),me=se(9),de=se(11),ue=e=>t=>ce(t)&&ne(t)===e,fe=(e,t,o)=>{if(!(r(o)||c(o)||u(o)))throw console.error("Invalid call to Attribute.set. Key ",t,":: Value ",o,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,o+"")},ge=(e,t,o)=>{fe(e.dom,t,o)},he=(e,t)=>{const o=e.dom;G(t,((e,t)=>{fe(o,t,e)}))},pe=(e,t)=>{const o=e.dom.getAttribute(t);return null===o?void 0:o},be=(e,t)=>C.from(pe(e,t)),we=(e,t)=>{e.dom.removeAttribute(t)},ve=e=>A(e.dom.attributes,((e,t)=>(e[t.name]=t.value,e)),{}),ye=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},xe={fromHtml:(e,t)=>{const o=(t||document).createElement("div");if(o.innerHTML=e,!o.hasChildNodes()||o.childNodes.length>1){const t="HTML does not have a single root node";throw console.error(t,e),new Error(t)}return ye(o.childNodes[0])},fromTag:(e,t)=>{const o=(t||document).createElement(e);return ye(o)},fromText:(e,t)=>{const o=(t||document).createTextNode(e);return ye(o)},fromDom:ye,fromPoint:(e,t,o)=>C.from(e.dom.elementFromPoint(t,o)).map(ye)},Ce=(e,t)=>{const o=e.dom;if(1!==o.nodeType)return!1;{const e=o;if(void 0!==e.matches)return e.matches(t);if(void 0!==e.msMatchesSelector)return e.msMatchesSelector(t);if(void 0!==e.webkitMatchesSelector)return e.webkitMatchesSelector(t);if(void 0!==e.mozMatchesSelector)return e.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")}},Se=e=>1!==e.nodeType&&9!==e.nodeType&&11!==e.nodeType||0===e.childElementCount,Te=(e,t)=>{const o=void 0===t?document:t.dom;return Se(o)?C.none():C.from(o.querySelector(e)).map(xe.fromDom)},Re=(e,t)=>e.dom===t.dom,De=(e,t)=>{const o=e.dom,n=t.dom;return o!==n&&o.contains(n)},Oe=Ce,ke=e=>xe.fromDom(e.dom.ownerDocument),Ee=e=>me(e)?e:ke(e),Ne=e=>C.from(e.dom.parentNode).map(xe.fromDom),_e=e=>C.from(e.dom.parentElement).map(xe.fromDom),Be=(e,t)=>{const o=d(t)?t:y;let n=e.dom;const r=[];for(;null!==n.parentNode&&void 0!==n.parentNode;){const e=n.parentNode,t=xe.fromDom(e);if(r.push(t),!0===o(t))break;n=e}return r},ze=e=>C.from(e.dom.previousSibling).map(xe.fromDom),Ae=e=>C.from(e.dom.nextSibling).map(xe.fromDom),Le=e=>E(e.dom.childNodes,xe.fromDom),We=(e,t)=>{const o=e.dom.childNodes;return C.from(o[t]).map(xe.fromDom)},Me=(e,t)=>{Ne(e).each((o=>{o.dom.insertBefore(t.dom,e.dom)}))},je=(e,t)=>{Ae(e).fold((()=>{Ne(e).each((e=>{Ie(e,t)}))}),(e=>{Me(e,t)}))},Pe=(e,t)=>{const o=(e=>We(e,0))(e);o.fold((()=>{Ie(e,t)}),(o=>{e.dom.insertBefore(t.dom,o.dom)}))},Ie=(e,t)=>{e.dom.appendChild(t.dom)},Fe=(e,t)=>{Me(e,t),Ie(t,e)},He=(e,t)=>{N(t,((o,n)=>{const r=0===n?e:t[n-1];je(r,o)}))},$e=(e,t)=>{N(t,(t=>{Ie(e,t)}))},Ve=e=>{e.dom.textContent="",N(Le(e),(e=>{qe(e)}))},qe=e=>{const t=e.dom;null!==t.parentNode&&t.parentNode.removeChild(t)},Ue=e=>{const t=Le(e);t.length>0&&He(e,t),qe(e)},Ge=(e,t)=>xe.fromDom(e.dom.cloneNode(t)),Ke=e=>Ge(e,!1),Ye=e=>Ge(e,!0),Je=(e,t)=>{const o=xe.fromTag(t),n=ve(e);return he(o,n),o},Qe=["tfoot","thead","tbody","colgroup"],Xe=(e,t,o)=>({element:e,rowspan:t,colspan:o}),Ze=(e,t,o)=>({element:e,cells:t,section:o}),et=(e,t,o)=>({element:e,isNew:t,isLocked:o}),tt=(e,t,o,n)=>({element:e,cells:t,section:o,isNew:n}),ot=e=>de(e)&&m(e.dom.host),nt=e=>xe.fromDom(e.dom.getRootNode()),rt=e=>xe.fromDom(e.dom.host),st=e=>{const t=ie(e)?e.dom.parentNode:e.dom;if(null==t||null===t.ownerDocument)return!1;const o=t.ownerDocument;return(e=>{const t=nt(e);return ot(t)?C.some(t):C.none()})(xe.fromDom(t)).fold((()=>o.body.contains(t)),(n=st,r=rt,e=>n(r(e))));var n,r},lt=()=>at(xe.fromDom(document)),at=e=>{const t=e.dom.body;if(null==t)throw new Error("Body is not available yet");return xe.fromDom(t)},ct=(e,t)=>{let o=[];return N(Le(e),(e=>{t(e)&&(o=o.concat([e])),o=o.concat(ct(e,t))})),o},it=(e,t,o)=>((e,o,n)=>B(Be(e,n),(e=>Ce(e,t))))(e,0,o),mt=(e,t)=>(e=>B(Le(e),(e=>Ce(e,t))))(e),dt=(e,t)=>((e,t)=>{const o=void 0===t?document:t.dom;return Se(o)?[]:E(o.querySelectorAll(e),xe.fromDom)})(t,e);var ut=(e,t,o,n,r)=>e(o,n)?C.some(o):d(r)&&r(o)?C.none():t(o,n,r);const ft=(e,t,o)=>{let n=e.dom;const r=d(o)?o:y;for(;n.parentNode;){n=n.parentNode;const e=xe.fromDom(n);if(t(e))return C.some(e);if(r(e))break}return C.none()},gt=(e,t,o)=>ut(((e,t)=>t(e)),ft,e,t,o),ht=(e,t,o)=>ft(e,(e=>Ce(e,t)),o),pt=(e,t)=>(e=>L(e.dom.childNodes,(e=>{return o=xe.fromDom(e),Ce(o,t);var o})).map(xe.fromDom))(e),bt=(e,t)=>Te(t,e),wt=(e,t,o)=>ut(((e,t)=>Ce(e,t)),ht,e,t,o),vt=(e,t,o=p)=>e.exists((e=>o(e,t))),yt=e=>{const t=[],o=e=>{t.push(e)};for(let t=0;te?C.some(t):C.none(),Ct=(e,t,o)=>""===t||e.length>=t.length&&e.substr(o,o+t.length)===t,St=(e,t,o=0,n)=>{const r=e.indexOf(t,o);return-1!==r&&(!!i(n)||r+t.length<=n)},Tt=(e,t)=>Ct(e,t,0),Rt=(e,t)=>Ct(e,t,e.length-t.length),Dt=(e=>t=>t.replace(e,""))(/^\s+|\s+$/g),Ot=e=>e.length>0,kt=e=>void 0!==e.style&&d(e.style.getPropertyValue),Et=(e,t,o)=>{if(!r(o))throw console.error("Invalid call to CSS.set. Property ",t,":: Value ",o,":: Element ",e),new Error("CSS value must be a string: "+o);kt(e)&&e.style.setProperty(t,o)},Nt=(e,t,o)=>{const n=e.dom;Et(n,t,o)},_t=(e,t)=>{const o=e.dom;G(t,((e,t)=>{Et(o,t,e)}))},Bt=(e,t)=>{const o=e.dom,n=window.getComputedStyle(o).getPropertyValue(t);return""!==n||st(e)?n:zt(o,t)},zt=(e,t)=>kt(e)?e.style.getPropertyValue(t):"",At=(e,t)=>{const o=e.dom,n=zt(o,t);return C.from(n).filter((e=>e.length>0))},Lt=(e,t)=>{((e,t)=>{kt(e)&&e.style.removeProperty(t)})(e.dom,t),vt(be(e,"style").map(Dt),"")&&we(e,"style")},Wt=(e,t,o=0)=>be(e,t).map((e=>parseInt(e,10))).getOr(o),Mt=(e,t)=>Wt(e,t,1),jt=e=>ue("col")(e)?Wt(e,"span",1)>1:Mt(e,"colspan")>1,Pt=(e,t)=>parseInt(Bt(e,t),10),It=g(10),Ft=g(10),Ht=(e,t)=>$t(e,t,x),$t=(e,t,o)=>j(Le(e),(e=>Ce(e,t)?o(e)?[e]:[]:$t(e,t,o))),Vt=(e,t)=>((e,t,o=y)=>o(t)?C.none():D(e,ne(t))?C.some(t):ht(t,e.join(","),(e=>Ce(e,"table")||o(e))))(["td","th"],e,t),qt=e=>Ht(e,"th,td"),Ut=e=>Ce(e,"colgroup")?mt(e,"col"):j(Yt(e),(e=>mt(e,"col"))),Gt=(e,t)=>wt(e,"table",t),Kt=e=>Ht(e,"tr"),Yt=e=>Gt(e).fold(g([]),(e=>mt(e,"colgroup"))),Jt=(e,t)=>E(e,(e=>{if("colgroup"===ne(e)){const t=E(Ut(e),(e=>{const t=Wt(e,"span",1);return Xe(e,1,t)}));return Ze(e,t,"colgroup")}{const o=E(qt(e),(e=>{const t=Wt(e,"rowspan",1),o=Wt(e,"colspan",1);return Xe(e,t,o)}));return Ze(e,o,t(e))}})),Qt=e=>Ne(e).map((e=>{const t=ne(e);return(e=>D(Qe,e))(t)?t:"tbody"})).getOr("tbody"),Xt=e=>{const t=Kt(e),o=[...Yt(e),...t];return Jt(o,Qt)},Zt=e=>{let t,o=!1;return(...n)=>(o||(o=!0,t=e.apply(null,n)),t)},eo=()=>to(0,0),to=(e,t)=>({major:e,minor:t}),oo={nu:to,detect:(e,t)=>{const o=String(t).toLowerCase();return 0===e.length?eo():((e,t)=>{const o=((e,t)=>{for(let o=0;oNumber(t.replace(o,"$"+e));return to(n(1),n(2))})(e,o)},unknown:eo},no=(e,t)=>{const o=String(t).toLowerCase();return L(e,(e=>e.search(o)))},ro=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,so=e=>t=>St(t,e),lo=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:e=>St(e,"edge/")&&St(e,"chrome")&&St(e,"safari")&&St(e,"applewebkit")},{name:"Chromium",brand:"Chromium",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,ro],search:e=>St(e,"chrome")&&!St(e,"chromeframe")},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:e=>St(e,"msie")||St(e,"trident")},{name:"Opera",versionRegexes:[ro,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:so("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:so("firefox")},{name:"Safari",versionRegexes:[ro,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:e=>(St(e,"safari")||St(e,"mobile/"))&&St(e,"applewebkit")}],ao=[{name:"Windows",search:so("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:e=>St(e,"iphone")||St(e,"ipad"),versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:so("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"macOS",search:so("mac os x"),versionRegexes:[/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:so("linux"),versionRegexes:[]},{name:"Solaris",search:so("sunos"),versionRegexes:[]},{name:"FreeBSD",search:so("freebsd"),versionRegexes:[]},{name:"ChromeOS",search:so("cros"),versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/]}],co={browsers:g(lo),oses:g(ao)},io="Edge",mo="Chromium",uo="Opera",fo="Firefox",go="Safari",ho=e=>{const t=e.current,o=e.version,n=e=>()=>t===e;return{current:t,version:o,isEdge:n(io),isChromium:n(mo),isIE:n("IE"),isOpera:n(uo),isFirefox:n(fo),isSafari:n(go)}},po=()=>ho({current:void 0,version:oo.unknown()}),bo=ho,wo=(g(io),g(mo),g("IE"),g(uo),g(fo),g(go),"Windows"),vo="Android",yo="Linux",xo="macOS",Co="Solaris",So="FreeBSD",To="ChromeOS",Ro=e=>{const t=e.current,o=e.version,n=e=>()=>t===e;return{current:t,version:o,isWindows:n(wo),isiOS:n("iOS"),isAndroid:n(vo),isMacOS:n(xo),isLinux:n(yo),isSolaris:n(Co),isFreeBSD:n(So),isChromeOS:n(To)}},Do=()=>Ro({current:void 0,version:oo.unknown()}),Oo=Ro,ko=(g(wo),g("iOS"),g(vo),g(yo),g(xo),g(Co),g(So),g(To),e=>window.matchMedia(e).matches);let Eo=Zt((()=>((e,t,o)=>{const n=co.browsers(),r=co.oses(),s=t.bind((e=>((e,t)=>V(t.brands,(t=>{const o=t.brand.toLowerCase();return L(e,(e=>{var t;return o===(null===(t=e.brand)||void 0===t?void 0:t.toLowerCase())})).map((e=>({current:e.name,version:oo.nu(parseInt(t.version,10),0)})))})))(n,e))).orThunk((()=>((e,t)=>no(e,t).map((e=>{const o=oo.detect(e.versionRegexes,t);return{current:e.name,version:o}})))(n,e))).fold(po,bo),l=((e,t)=>no(e,t).map((e=>{const o=oo.detect(e.versionRegexes,t);return{current:e.name,version:o}})))(r,e).fold(Do,Oo),a=((e,t,o,n)=>{const r=e.isiOS()&&!0===/ipad/i.test(o),s=e.isiOS()&&!r,l=e.isiOS()||e.isAndroid(),a=l||n("(pointer:coarse)"),c=r||!s&&l&&n("(min-device-width:768px)"),i=s||l&&!c,m=t.isSafari()&&e.isiOS()&&!1===/safari/i.test(o),d=!i&&!c&&!m;return{isiPad:g(r),isiPhone:g(s),isTablet:g(c),isPhone:g(i),isTouch:g(a),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:g(m),isDesktop:g(d)}})(l,s,e,o);return{browser:s,os:l,deviceType:a}})(window.navigator.userAgent,C.from(window.navigator.userAgentData),ko)));const No=()=>Eo(),_o=(e,t)=>{const o=o=>{const n=t(o);if(n<=0||null===n){const t=Bt(o,e);return parseFloat(t)||0}return n},n=(e,t)=>A(t,((t,o)=>{const n=Bt(e,o),r=void 0===n?0:parseInt(n,10);return isNaN(r)?t:t+r}),0);return{set:(t,o)=>{if(!u(o)&&!o.match(/^[0-9]+$/))throw new Error(e+".set accepts only positive integer values. Value was "+o);const n=t.dom;kt(n)&&(n.style[e]=o+"px")},get:o,getOuter:o,aggregate:n,max:(e,t,o)=>{const r=n(e,o);return t>r?t-r:0}}},Bo=(e,t,o)=>((e,t)=>(e=>{const t=parseFloat(e);return isNaN(t)?C.none():C.some(t)})(e).getOr(t))(Bt(e,t),o),zo=_o("width",(e=>e.dom.offsetWidth)),Ao=e=>zo.get(e),Lo=e=>zo.getOuter(e),Wo=e=>((e,t)=>{const o=e.dom,n=o.getBoundingClientRect().width||o.offsetWidth;return"border-box"===t?n:((e,t,o,n)=>t-Bo(e,`padding-${o}`,0)-Bo(e,`padding-${n}`,0)-Bo(e,`border-${o}-width`,0)-Bo(e,`border-${n}-width`,0))(e,n,"left","right")})(e,"content-box"),Mo=(e,t,o)=>{const n=e.cells,r=n.slice(0,t),s=n.slice(t),l=r.concat(o).concat(s);return Io(e,l)},jo=(e,t,o)=>Mo(e,t,[o]),Po=(e,t,o)=>{e.cells[t]=o},Io=(e,t)=>tt(e.element,t,e.section,e.isNew),Fo=(e,t)=>e.cells[t],Ho=(e,t)=>Fo(e,t).element,$o=e=>e.cells.length,Vo=e=>{const t=_(e,(e=>"colgroup"===e.section));return{rows:t.fail,cols:t.pass}},qo=(e,t,o)=>{const n=E(e.cells,o);return tt(t(e.element),n,e.section,!0)},Uo="data-snooker-locked-cols",Go=e=>be(e,Uo).bind((e=>C.from(e.match(/\d+/g)))).map((e=>I(e,x))),Ko=e=>{const t=A(Vo(e).rows,((e,t)=>(N(t.cells,((t,o)=>{t.isLocked&&(e[o]=!0)})),e)),{}),o=J(t,((e,t)=>parseInt(t,10)));return(e=>{const t=S.call(e,0);return t.sort(void 0),t})(o)},Yo=(e,t)=>e+","+t,Jo=(e,t)=>{const o=j(e.all,(e=>e.cells));return B(o,t)},Qo=e=>{const t={},o=[],n=H(e).map((e=>e.element)).bind(Gt).bind(Go).getOr({});let r=0,s=0,l=0;const{pass:a,fail:c}=_(e,(e=>"colgroup"===e.section));N(c,(e=>{const a=[];N(e.cells,(e=>{let o=0;for(;void 0!==t[Yo(l,o)];)o++;const r=((e,t)=>X(e,t)&&void 0!==e[t]&&null!==e[t])(n,o.toString()),c=((e,t,o,n,r,s)=>({element:e,rowspan:t,colspan:o,row:n,column:r,isLocked:s}))(e.element,e.rowspan,e.colspan,l,o,r);for(let n=0;n{const t=(e=>{const t={};let o=0;return N(e.cells,(e=>{const n=e.colspan;k(n,(r=>{const s=o+r;t[s]=((e,t,o)=>({element:e,colspan:t,column:o}))(e.element,n,s)})),o+=n})),t})(e),o=((e,t)=>({element:e,columns:t}))(e.element,Q(t));return{colgroups:[o],columns:t}})).getOrThunk((()=>({colgroups:[],columns:{}}))),d=((e,t)=>({rows:e,columns:t}))(r,s);return{grid:d,access:t,all:o,columns:i,colgroups:m}},Xo=e=>{const t=Xt(e);return Qo(t)},Zo=Qo,en=(e,t,o)=>C.from(e.access[Yo(t,o)]),tn=(e,t,o)=>{const n=Jo(e,(e=>o(t,e.element)));return n.length>0?C.some(n[0]):C.none()},on=Jo,nn=e=>j(e.all,(e=>e.cells)),rn=e=>Q(e.columns),sn=e=>q(e.columns).length>0,ln=(e,t)=>C.from(e.columns[t]),an=(e,t=x)=>{const o=e.grid,n=k(o.columns,h),r=k(o.rows,h);return E(n,(o=>cn((()=>j(r,(t=>en(e,t,o).filter((e=>e.column===o)).toArray()))),(e=>1===e.colspan&&t(e.element)),(()=>en(e,0,o)))))},cn=(e,t,o)=>{const n=e();return L(n,t).orThunk((()=>C.from(n[0]).orThunk(o))).map((e=>e.element))},mn=e=>{const t=e.grid,o=k(t.rows,h),n=k(t.columns,h);return E(o,(t=>cn((()=>j(n,(o=>en(e,t,o).filter((e=>e.row===t)).fold(g([]),(e=>[e]))))),(e=>1===e.rowspan),(()=>en(e,t,0)))))},dn=(e,t)=>o=>"rtl"===un(o)?t:e,un=e=>"rtl"===Bt(e,"direction")?"rtl":"ltr",fn=_o("height",(e=>{const t=e.dom;return st(e)?t.getBoundingClientRect().height:t.offsetHeight})),gn=e=>fn.get(e),hn=e=>fn.getOuter(e),pn=(e,t)=>({left:e,top:t,translate:(o,n)=>pn(e+o,t+n)}),bn=pn,wn=(e,t)=>void 0!==e?e:void 0!==t?t:0,vn=e=>{const t=e.dom.ownerDocument,o=t.body,n=t.defaultView,r=t.documentElement;if(o===e.dom)return bn(o.offsetLeft,o.offsetTop);const s=wn(null==n?void 0:n.pageYOffset,r.scrollTop),l=wn(null==n?void 0:n.pageXOffset,r.scrollLeft),a=wn(r.clientTop,o.clientTop),c=wn(r.clientLeft,o.clientLeft);return yn(e).translate(l-c,s-a)},yn=e=>{const t=e.dom,o=t.ownerDocument.body;return o===t?bn(o.offsetLeft,o.offsetTop):st(e)?(e=>{const t=e.getBoundingClientRect();return bn(t.left,t.top)})(t):bn(0,0)},xn=(e,t)=>({row:e,y:t}),Cn=(e,t)=>({col:e,x:t}),Sn=e=>vn(e).left+Lo(e),Tn=e=>vn(e).left,Rn=(e,t)=>Cn(e,Tn(t)),Dn=(e,t)=>Cn(e,Sn(t)),On=e=>vn(e).top,kn=(e,t)=>xn(e,On(t)),En=(e,t)=>xn(e,On(t)+hn(t)),Nn=(e,t,o)=>{if(0===o.length)return[];const n=E(o.slice(1),((t,o)=>t.map((t=>e(o,t))))),r=o[o.length-1].map((e=>t(o.length-1,e)));return n.concat([r])},_n={delta:h,positions:e=>Nn(kn,En,e),edge:On},Bn=dn({delta:h,edge:Tn,positions:e=>Nn(Rn,Dn,e)},{delta:e=>-e,edge:Sn,positions:e=>Nn(Dn,Rn,e)}),zn={delta:(e,t)=>Bn(t).delta(e,t),positions:(e,t)=>Bn(t).positions(e,t),edge:e=>Bn(e).edge(e)},An={unsupportedLength:["em","ex","cap","ch","ic","rem","lh","rlh","vw","vh","vi","vb","vmin","vmax","cm","mm","Q","in","pc","pt","px"],fixed:["px","pt"],relative:["%"],empty:[""]},Ln=(()=>{const e="[0-9]+",t="[eE][+-]?"+e,o=e=>`(?:${e})?`,n=["Infinity",e+"\\."+o(e)+o(t),"\\."+e+o(t),e+o(t)].join("|");return new RegExp(`^([+-]?(?:${n}))(.*)$`)})(),Wn=/(\d+(\.\d+)?)%/,Mn=/(\d+(\.\d+)?)px|em/,jn=ue("col"),Pn=ue("tr"),In=(e,t,o)=>{const n=_e(e).getOrThunk((()=>at(ke(e))));return t(e)/o(n)*100},Fn=(e,t)=>{Nt(e,"width",t+"px")},Hn=(e,t)=>{Nt(e,"width",t+"%")},$n=(e,t)=>{Nt(e,"height",t+"px")},Vn=e=>{const t=(e=>{return Bo(t=e,"height",t.dom.offsetHeight)+"px";var t})(e);return t?((e,t,o,n)=>{const r=parseFloat(e);return Rt(e,"%")&&"table"!==ne(t)?((e,t,o,n)=>{const r=Gt(e).map((e=>{const n=o(e);return Math.floor(t/100*n)})).getOr(t);return n(e,r),r})(t,r,o,n):r})(t,e,gn,$n):gn(e)},qn=(e,t)=>At(e,t).orThunk((()=>be(e,t).map((e=>e+"px")))),Un=e=>qn(e,"width"),Gn=e=>In(e,Ao,Wo),Kn=e=>{return jn(e)?Ao(e):Bo(t=e,"width",t.dom.offsetWidth);var t},Yn=e=>Pn(e)?gn(e):((e,t,o)=>o(e)/Mt(e,"rowspan"))(e,0,Vn),Jn=(e,t,o)=>{Nt(e,"width",t+o)},Qn=e=>In(e,Ao,Wo)+"%",Xn=g(Wn),Zn=ue("col"),er=e=>Un(e).getOrThunk((()=>Kn(e)+"px")),tr=e=>{return(t=e,qn(t,"height")).getOrThunk((()=>Yn(e)+"px"));var t},or=(e,t,o,n,r,s)=>e.filter(n).fold((()=>s(((e,t)=>{if(t<0||t>=e.length-1)return C.none();const o=e[t].fold((()=>{const o=(e=>{const t=S.call(e,0);return t.reverse(),t})(e.slice(0,t));return V(o,((e,t)=>e.map((e=>({value:e,delta:t+1})))))}),(e=>C.some({value:e,delta:0}))),n=e[t+1].fold((()=>{const o=e.slice(t+1);return V(o,((e,t)=>e.map((e=>({value:e,delta:t+1})))))}),(e=>C.some({value:e,delta:1})));return o.bind((e=>n.map((t=>{const o=t.delta+e.delta;return Math.abs(t.value-e.value)/o}))))})(o,t))),(e=>r(e))),nr=(e,t,o,n)=>{const r=an(e),s=sn(e)?(e=>E(rn(e),(e=>C.from(e.element))))(e):r,l=[C.some(zn.edge(t))].concat(E(zn.positions(r,t),(e=>e.map((e=>e.x))))),a=w(jt);return E(s,((e,t)=>or(e,t,l,a,(e=>{if((e=>{const t=No().browser,o=t.isChromium()||t.isFirefox();return!Zn(e)||o})(e))return o(e);{const e=null!=(s=r[t])?h(s):C.none();return or(e,t,l,a,(e=>n(C.some(Ao(e)))),n)}var s}),n)))},rr=e=>e.map((e=>e+"px")).getOr(""),sr=(e,t,o)=>nr(e,t,Kn,(e=>e.getOrThunk(o.minCellWidth))),lr=(e,t,o,n)=>{const r=mn(e),s=E(e.all,(e=>C.some(e.element))),l=[C.some(_n.edge(t))].concat(E(_n.positions(r,t),(e=>e.map((e=>e.y)))));return E(s,((e,t)=>or(e,t,l,x,o,n)))},ar=(e,t)=>()=>st(e)?t(e):parseFloat(At(e,"width").getOr("0")),cr=e=>{const t=ar(e,(e=>parseFloat(Qn(e)))),o=ar(e,Ao);return{width:t,pixelWidth:o,getWidths:(t,o)=>((e,t,o)=>nr(e,t,Gn,(e=>e.fold((()=>o.minCellWidth()),(e=>e/o.pixelWidth()*100)))))(t,e,o),getCellDelta:e=>e/o()*100,singleColumnWidth:(e,t)=>[100-e],minCellWidth:()=>It()/o()*100,setElementWidth:Hn,adjustTableWidth:o=>{const n=t();Hn(e,n+o/100*n)},isRelative:!0,label:"percent"}},ir=e=>{const t=ar(e,Ao);return{width:t,pixelWidth:t,getWidths:(t,o)=>sr(t,e,o),getCellDelta:h,singleColumnWidth:(e,t)=>[Math.max(It(),e+t)-e],minCellWidth:It,setElementWidth:Fn,adjustTableWidth:o=>{const n=t()+o;Fn(e,n)},isRelative:!1,label:"pixel"}},mr=e=>Un(e).fold((()=>(e=>{const t=ar(e,Ao),o=g(0);return{width:t,pixelWidth:t,getWidths:(t,o)=>sr(t,e,o),getCellDelta:o,singleColumnWidth:g([0]),minCellWidth:o,setElementWidth:f,adjustTableWidth:f,isRelative:!0,label:"none"}})(e)),(t=>((e,t)=>null!==Xn().exec(t)?cr(e):ir(e))(e,t))),dr=ir,ur=cr,fr=(e,t,o)=>{const n=e[o].element,r=xe.fromTag("td");Ie(r,xe.fromTag("br")),(t?Ie:Pe)(n,r)},gr=(e=>{const t=t=>e(t)?C.from(t.dom.nodeValue):C.none();return{get:o=>{if(!e(o))throw new Error("Can only get text value of a text node");return t(o).getOr("")},getOption:t,set:(t,o)=>{if(!e(t))throw new Error("Can only set raw text value of a text node");t.dom.nodeValue=o}}})(ie),hr=e=>gr.get(e),pr=e=>gr.getOption(e),br=(e,t)=>gr.set(e,t),wr=e=>"img"===ne(e)?1:pr(e).fold((()=>Le(e).length),(e=>e.length)),vr=["img","br"],yr=e=>pr(e).filter((e=>0!==e.trim().length||e.indexOf("\xa0")>-1)).isSome()||D(vr,ne(e))||(e=>ae(e)&&"false"===pe(e,"contenteditable"))(e),xr=e=>((e,t)=>{const o=e=>{for(let n=0;nSr(e,yr),Sr=(e,t)=>{const o=e=>{const n=Le(e);for(let e=n.length-1;e>=0;e--){const r=n[e];if(t(r))return C.some(r);const s=o(r);if(s.isSome())return s}return C.none()};return o(e)},Tr={scope:["row","col"]},Rr=e=>()=>{const t=xe.fromTag("td",e.dom);return Ie(t,xe.fromTag("br",e.dom)),t},Dr=e=>()=>xe.fromTag("col",e.dom),Or=e=>()=>xe.fromTag("colgroup",e.dom),kr=e=>()=>xe.fromTag("tr",e.dom),Er=(e,t,o)=>{const n=((e,t)=>{const o=Je(e,t),n=Le(Ye(e));return $e(o,n),o})(e,t);return G(o,((e,t)=>{null===e?we(n,t):ge(n,t,e)})),n},Nr=e=>e,_r=(e,t,o)=>{const n=(e,t)=>{((e,t)=>{const o=e.dom,n=t.dom;kt(o)&&kt(n)&&(n.style.cssText=o.style.cssText)})(e.element,t),Lt(t,"height"),1!==e.colspan&&Lt(t,"width")};return{col:o=>{const r=xe.fromTag(ne(o.element),t.dom);return n(o,r),e(o.element,r),r},colgroup:Or(t),row:kr(t),cell:r=>{const s=xe.fromTag(ne(r.element),t.dom),l=o.getOr(["strong","em","b","i","span","font","h1","h2","h3","h4","h5","h6","p","div"]),a=l.length>0?((e,t,o)=>xr(e).map((n=>{const r=o.join(","),s=it(n,r,(t=>Re(t,e)));return z(s,((e,t)=>{const o=Ke(t);return Ie(e,o),o}),t)})).getOr(t))(r.element,s,l):s;return Ie(a,xe.fromTag("br")),n(r,s),((e,t)=>{G(Tr,((o,n)=>be(e,n).filter((e=>D(o,e))).each((e=>ge(t,n,e)))))})(r.element,s),e(r.element,s),s},replace:Er,colGap:Dr(t),gap:Rr(t)}},Br=e=>({col:Dr(e),colgroup:Or(e),row:kr(e),cell:Rr(e),replace:Nr,colGap:Dr(e),gap:Rr(e)}),zr=e=>t=>t.options.get(e),Ar="100%",Lr=e=>{var t;const o=e.dom,n=null!==(t=o.getParent(e.selection.getStart(),o.isBlock))&&void 0!==t?t:e.getBody();return Wo(xe.fromDom(n))+"px"},Wr=e=>C.from(e.options.get("table_clone_elements")),Mr=zr("table_header_type"),jr=zr("table_column_resizing"),Pr=e=>"preservetable"===jr(e),Ir=e=>"resizetable"===jr(e),Fr=zr("table_sizing_mode"),Hr=e=>"relative"===Fr(e),$r=e=>"fixed"===Fr(e),Vr=e=>"responsive"===Fr(e),qr=zr("table_resize_bars"),Ur=zr("table_style_by_css"),Gr=zr("table_merge_content_on_paste"),Kr=e=>{const t=e.options,o=t.get("table_default_attributes");return t.isSet("table_default_attributes")?o:((e,t)=>Vr(e)||Ur(e)?t:$r(e)?{...t,width:Lr(e)}:{...t,width:Ar})(e,o)},Yr=zr("table_use_colgroups"),Jr=zr("fixed_toolbar_container"),Qr=zr("fixed_toolbar_container_target"),Xr=zr("ui_mode"),Zr=e=>wt(e,"[contenteditable]"),es=(e,t=!1)=>st(e)?e.dom.isContentEditable:Zr(e).fold(g(t),(e=>"true"===ts(e))),ts=e=>e.dom.contentEditable,os=e=>xe.fromDom(e.getBody()),ns=e=>t=>Re(t,os(e)),rs=e=>{we(e,"data-mce-style");const t=e=>we(e,"data-mce-style");N(qt(e),t),N(Ut(e),t),N(Kt(e),t)},ss=e=>xe.fromDom(e.selection.getStart()),ls=e=>e.getBoundingClientRect().width,as=e=>e.getBoundingClientRect().height,cs=e=>(t,o)=>{const n=t.dom.getStyle(o,e)||t.dom.getAttrib(o,e);return C.from(n).filter(Ot)},is=cs("width"),ms=cs("height"),ds=e=>gt(e,ue("table")).exists(es),us=(e,t)=>{const o=t.column,n=t.column+t.colspan-1,r=t.row,s=t.row+t.rowspan-1;return o<=e.finishCol&&n>=e.startCol&&r<=e.finishRow&&s>=e.startRow},fs=(e,t)=>t.column>=e.startCol&&t.column+t.colspan-1<=e.finishCol&&t.row>=e.startRow&&t.row+t.rowspan-1<=e.finishRow,gs=(e,t,o)=>{const n=tn(e,t,Re),r=tn(e,o,Re);return n.bind((e=>r.map((t=>{return o=e,n=t,{startRow:Math.min(o.row,n.row),startCol:Math.min(o.column,n.column),finishRow:Math.max(o.row+o.rowspan-1,n.row+n.rowspan-1),finishCol:Math.max(o.column+o.colspan-1,n.column+n.colspan-1)};var o,n}))))},hs=(e,t,o)=>gs(e,t,o).map((t=>{const o=on(e,b(us,t));return E(o,(e=>e.element))})),ps=(e,t)=>tn(e,t,((e,t)=>De(t,e))).map((e=>e.element)),bs=(e,t,o)=>{const n=vs(e);return hs(n,t,o)},ws=(e,t,o,n,r)=>{const s=vs(e),l=Re(e,o)?C.some(t):ps(s,t),a=Re(e,r)?C.some(n):ps(s,n);return l.bind((e=>a.bind((t=>hs(s,e,t)))))},vs=Xo;var ys=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","li","table","thead","tbody","tfoot","caption","tr","td","th","h1","h2","h3","h4","h5","h6","blockquote","pre","address"],xs=()=>({up:g({selector:ht,closest:wt,predicate:ft,all:Be}),down:g({selector:dt,predicate:ct}),styles:g({get:Bt,getRaw:At,set:Nt,remove:Lt}),attrs:g({get:pe,set:ge,remove:we,copyTo:(e,t)=>{const o=ve(e);he(t,o)}}),insert:g({before:Me,after:je,afterAll:He,append:Ie,appendAll:$e,prepend:Pe,wrap:Fe}),remove:g({unwrap:Ue,remove:qe}),create:g({nu:xe.fromTag,clone:e=>xe.fromDom(e.dom.cloneNode(!1)),text:xe.fromText}),query:g({comparePosition:(e,t)=>e.dom.compareDocumentPosition(t.dom),prevSibling:ze,nextSibling:Ae}),property:g({children:Le,name:ne,parent:Ne,document:e=>Ee(e).dom,isText:ie,isComment:le,isElement:ce,isSpecial:e=>{const t=ne(e);return D(["script","noscript","iframe","noframes","noembed","title","style","textarea","xmp"],t)},getLanguage:e=>ce(e)?be(e,"lang"):C.none(),getText:hr,setText:br,isBoundary:e=>!!ce(e)&&("body"===ne(e)||D(ys,ne(e))),isEmptyTag:e=>!!ce(e)&&D(["br","img","hr","input"],ne(e)),isNonEditable:e=>ce(e)&&"false"===pe(e,"contenteditable")}),eq:Re,is:Oe});const Cs=(e,t,o,n)=>{const r=t(e,o);return z(n,((o,n)=>{const r=t(e,n);return Ss(e,o,r)}),r)},Ss=(e,t,o)=>t.bind((t=>o.filter(b(e.eq,t)))),Ts=xs(),Rs=(e,t)=>((e,t,o)=>o.length>0?((e,t,o,n)=>n(e,t,o[0],o.slice(1)))(e,t,o,Cs):C.none())(Ts,((t,o)=>e(o)),t),Ds=e=>ht(e,"table"),Os=(e,t,o)=>{const n=e=>t=>void 0!==o&&o(t)||Re(t,e);return Re(e,t)?C.some({boxes:C.some([e]),start:e,finish:t}):Ds(e).bind((r=>Ds(t).bind((s=>{if(Re(r,s))return C.some({boxes:bs(r,e,t),start:e,finish:t});if(De(r,s)){const o=it(t,"td,th",n(r)),l=o.length>0?o[o.length-1]:t;return C.some({boxes:ws(r,e,r,t,s),start:e,finish:l})}if(De(s,r)){const o=it(e,"td,th",n(s)),l=o.length>0?o[o.length-1]:e;return C.some({boxes:ws(s,e,r,t,s),start:e,finish:l})}return((e,t)=>((e,t,o,n=y)=>{const r=[t].concat(e.up().all(t)),s=[o].concat(e.up().all(o)),l=e=>W(e,n).fold((()=>e),(t=>e.slice(0,t+1))),a=l(r),c=l(s),i=L(a,(t=>O(c,((e,t)=>b(e.eq,t))(e,t))));return{firstpath:a,secondpath:c,shared:i}})(Ts,e,t,void 0))(e,t).shared.bind((l=>wt(l,"table",o).bind((o=>{const l=it(t,"td,th",n(o)),a=l.length>0?l[l.length-1]:t,c=it(e,"td,th",n(o)),i=c.length>0?c[c.length-1]:e;return C.some({boxes:ws(o,e,r,t,s),start:i,finish:a})}))))}))))},ks=(e,t)=>{const o=dt(e,t);return o.length>0?C.some(o):C.none()},Es=(e,t,o)=>bt(e,t).bind((t=>bt(e,o).bind((e=>Rs(Ds,[t,e]).map((o=>({first:t,last:e,table:o}))))))),Ns=(e,t,o,n,r)=>((e,t)=>L(e,(e=>Ce(e,t))))(e,r).bind((e=>((e,t,o)=>Gt(e).bind((n=>((e,t,o,n)=>tn(e,t,Re).bind((t=>{const r=o>0?t.row+t.rowspan-1:t.row,s=n>0?t.column+t.colspan-1:t.column;return en(e,r+o,s+n).map((e=>e.element))})))(vs(n),e,t,o))))(e,t,o).bind((e=>((e,t)=>ht(e,"table").bind((o=>bt(o,t).bind((t=>Os(t,e).bind((e=>e.boxes.map((t=>({boxes:t,start:e.start,finish:e.finish}))))))))))(e,n))))),_s=(e,t)=>ks(e,t),Bs=(e,t,o)=>Es(e,t,o).bind((t=>{const o=t=>Re(e,t),n="thead,tfoot,tbody,table",r=ht(t.first,n,o),s=ht(t.last,n,o);return r.bind((e=>s.bind((o=>Re(e,o)?((e,t,o)=>((e,t,o)=>gs(e,t,o).bind((t=>((e,t)=>{let o=!0;const n=b(fs,t);for(let r=t.startRow;r<=t.finishRow;r++)for(let s=t.startCol;s<=t.finishCol;s++)o=o&&en(e,r,s).exists(n);return o?C.some(t):C.none()})(e,t))))(vs(e),t,o))(t.table,t.first,t.last):C.none()))))})),zs=h,As=e=>{const t=(e,t)=>be(e,t).exists((e=>parseInt(e,10)>1));return e.length>0&&P(e,(e=>t(e,"rowspan")||t(e,"colspan")))?C.some(e):C.none()},Ls=(e,t,o)=>t.length<=1?C.none():Bs(e,o.firstSelectedSelector,o.lastSelectedSelector).map((e=>({bounds:e,cells:t}))),Ws="data-mce-selected",Ms="data-mce-first-selected",js="data-mce-last-selected",Ps="["+Ws+"]",Is={selected:Ws,selectedSelector:"td["+Ws+"],th["+Ws+"]",firstSelected:Ms,firstSelectedSelector:"td["+Ms+"],th["+Ms+"]",lastSelected:js,lastSelectedSelector:"td["+js+"],th["+js+"]"},Fs=(e,t,o)=>({element:o,mergable:Ls(t,e,Is),unmergable:As(e),selection:zs(e)}),Hs=e=>(t,o)=>{const n=ne(t),r="col"===n||"colgroup"===n?Gt(s=t).bind((e=>_s(e,Is.firstSelectedSelector))).fold(g(s),(e=>e[0])):t;var s;return wt(r,e,o)},$s=Hs("th,td,caption"),Vs=Hs("th,td"),qs=e=>{return t=e.model.table.getSelectedCells(),E(t,xe.fromDom);var t},Us=(e,t)=>{e.on("BeforeGetContent",(t=>{const o=o=>{t.preventDefault(),(e=>Gt(e[0]).map((e=>{const t=((e,t)=>{const o=e=>Ce(e.element,t),n=Ye(e),r=Xt(n),s=mr(e),l=Zo(r),a=((e,t)=>{const o=e.grid.columns;let n=e.grid.rows,r=o,s=0,l=0;const a=[],c=[];return G(e.access,(e=>{if(a.push(e),t(e)){c.push(e);const t=e.row,o=t+e.rowspan-1,a=e.column,i=a+e.colspan-1;ts&&(s=o),al&&(l=i)}})),((e,t,o,n,r,s)=>({minRow:e,minCol:t,maxRow:o,maxCol:n,allCells:r,selectedCells:s}))(n,r,s,l,a,c)})(l,o),c="th:not("+t+"),td:not("+t+")",i=$t(n,"th,td",(e=>Ce(e,c)));N(i,qe),((e,t,o,n)=>{const r=B(e,(e=>"colgroup"!==e.section)),s=t.grid.columns,l=t.grid.rows;for(let e=0;eo.maxRow||ao.maxCol||(en(t,e,a).filter(n).isNone()?fr(r,l,e):l=!0)}})(r,l,a,o);const m=((e,t,o,n)=>{if(0===n.minCol&&t.grid.columns===n.maxCol+1)return 0;const r=sr(t,e,o),s=A(r,((e,t)=>e+t),0),l=A(r.slice(n.minCol,n.maxCol+1),((e,t)=>e+t),0),a=l/s*o.pixelWidth()-o.pixelWidth();return o.getCellDelta(a)})(e,Xo(e),s,a);return((e,t,o,n)=>{G(o.columns,(e=>{(e.columnt.maxCol)&&qe(e.element)}));const r=B(Ht(e,"tr"),(e=>0===e.dom.childElementCount));N(r,qe),t.minCol!==t.maxCol&&t.minRow!==t.maxRow||N(Ht(e,"th,td"),(e=>{we(e,"rowspan"),we(e,"colspan")})),we(e,Uo),we(e,"data-snooker-col-series"),mr(e).adjustTableWidth(n)})(n,a,l,m),n})(e,Ps);return rs(t),[t]})))(o).each((o=>{const n="text"===t.format?((e,t)=>{const o=e.getDoc(),n=nt(xe.fromDom(e.getBody())),r=xe.fromTag("div",o);ge(r,"data-mce-bogus","all"),_t(r,{position:"fixed",left:"-9999999px",top:"0",overflow:"hidden",opacity:"0"});const s=(e=>ot(e)?e:xe.fromDom(Ee(e).dom.body))(n);$e(r,t),Ie(s,r);const l=r.dom.innerText;return qe(r),l})(e,o):((e,t)=>E(t,(t=>e.selection.serializer.serialize(t.dom,{}))).join(""))(e,o);t.content=n}))};if(!0===t.selection){const t=(e=>B(qs(e),(e=>Ce(e,Is.selectedSelector))))(e);t.length>=1&&o(t)}})),e.on("BeforeSetContent",(o=>{if(!0===o.selection&&!0===o.paste){const n=qs(e);H(n).each((n=>{Gt(n).each((r=>{const s=B((e=>{const t=document.createElement("div");return t.innerHTML=e,Le(xe.fromDom(t))})(o.content),(e=>"meta"!==ne(e))),l=ue("table");if(Gr(e)&&1===s.length&&l(s[0])){o.preventDefault();const l=xe.fromDom(e.getDoc()),a=Br(l),c=((e,t,o)=>({element:e,clipboard:t,generators:o}))(n,s[0],a);t.pasteCells(r,c).each((()=>{e.focus()}))}}))}))}}))},Gs=(e,t)=>({element:e,offset:t}),Ks=(e,t,o)=>e.property().isText(t)&&0===e.property().getText(t).trim().length||e.property().isComment(t)?o(t).bind((t=>Ks(e,t,o).orThunk((()=>C.some(t))))):C.none(),Ys=(e,t)=>e.property().isText(t)?e.property().getText(t).length:e.property().children(t).length,Js=(e,t)=>{const o=Ks(e,t,e.query().prevSibling).getOr(t);if(e.property().isText(o))return Gs(o,Ys(e,o));const n=e.property().children(o);return n.length>0?Js(e,n[n.length-1]):Gs(o,Ys(e,o))},Qs=Js,Xs=xs(),Zs=(e,t)=>{if(!jt(e)){const o=(e=>Un(e).bind((e=>{return t=e,o=["fixed","relative","empty"],C.from(Ln.exec(t)).bind((e=>{const t=Number(e[1]),n=e[2];return((e,t)=>O(t,(t=>O(An[t],(t=>e===t)))))(n,o)?C.some({value:t,unit:n}):C.none()}));var t,o})))(e);o.each((o=>{const n=o.value/2;Jn(e,n,o.unit),Jn(t,n,o.unit)}))}},el=e=>E(e,g(0)),tl=(e,t,o,n,r)=>r(e.slice(0,t)).concat(n).concat(r(e.slice(o))),ol=e=>(t,o,n,r)=>{if(e(n)){const e=Math.max(r,t[o]-Math.abs(n)),s=Math.abs(e-t[o]);return n>=0?s:-s}return n},nl=ol((e=>e<0)),rl=ol(x),sl=()=>{const e=(e,t,o,n)=>{const r=(100+o)/100,s=Math.max(n,(e[t]+o)/r);return E(e,((e,o)=>(o===t?s:e/r)-e))},t=(t,o,n,r,s,l)=>l?e(t,o,r,s):((e,t,o,n,r)=>{const s=nl(e,t,n,r);return tl(e,t,o+1,[s,0],el)})(t,o,n,r,s);return{resizeTable:(e,t)=>e(t),clampTableDelta:nl,calcLeftEdgeDeltas:t,calcMiddleDeltas:(e,o,n,r,s,l,a)=>t(e,n,r,s,l,a),calcRightEdgeDeltas:(t,o,n,r,s,l)=>{if(l)return e(t,n,r,s);{const e=nl(t,n,r,s);return el(t.slice(0,n)).concat([e])}},calcRedestributedWidths:(e,t,o,n)=>{if(n){const n=(t+o)/t,r=E(e,(e=>e/n));return{delta:100*n-100,newSizes:r}}return{delta:o,newSizes:e}}}},ll=()=>{const e=(e,t,o,n,r)=>{const s=rl(e,n>=0?o:t,n,r);return tl(e,t,o+1,[s,-s],el)};return{resizeTable:(e,t,o)=>{o&&e(t)},clampTableDelta:(e,t,o,n,r)=>{if(r){if(o>=0)return o;{const t=A(e,((e,t)=>e+t-n),0);return Math.max(-t,o)}}return nl(e,t,o,n)},calcLeftEdgeDeltas:e,calcMiddleDeltas:(t,o,n,r,s,l)=>e(t,n,r,s,l),calcRightEdgeDeltas:(e,t,o,n,r,s)=>{if(s)return el(e);{const t=n/e.length;return E(e,g(t))}},calcRedestributedWidths:(e,t,o,n)=>({delta:0,newSizes:e})}},al=e=>Xo(e).grid,cl=ue("th"),il=e=>P(e,(e=>cl(e.element))),ml=(e,t)=>e&&t?"sectionCells":e?"section":"cells",dl=e=>{const t="thead"===e.section,o=vt(ul(e.cells),"th");return"tfoot"===e.section?{type:"footer"}:t||o?{type:"header",subType:ml(t,o)}:{type:"body"}},ul=e=>{const t=B(e,(e=>cl(e.element)));return 0===t.length?C.some("td"):t.length===e.length?C.some("th"):C.none()},fl=(e,t,o)=>et(o(e.element,t),!0,e.isLocked),gl=(e,t)=>e.section!==t?tt(e.element,e.cells,t,e.isNew):e,hl=()=>({transformRow:gl,transformCell:(e,t,o)=>{const n=o(e.element,t),r="td"!==ne(n)?(e=>{const t=Je(e,"td");je(e,t);const o=Le(e);return $e(t,o),qe(e),t})(n):n;return et(r,e.isNew,e.isLocked)}}),pl=()=>({transformRow:gl,transformCell:fl}),bl=()=>({transformRow:(e,t)=>gl(e,"thead"===t?"tbody":t),transformCell:fl}),wl=hl,vl=pl,yl=bl,xl=()=>({transformRow:h,transformCell:fl}),Cl=(e,t,o,n)=>{o===n?we(e,t):ge(e,t,o)},Sl=(e,t,o)=>{$(mt(e,t)).fold((()=>Pe(e,o)),(e=>je(e,o)))},Tl=(e,t)=>{const o=[],n=[],r=e=>E(e,(e=>{e.isNew&&o.push(e.element);const t=e.element;return Ve(t),N(e.cells,(e=>{e.isNew&&n.push(e.element),Cl(e.element,"colspan",e.colspan,1),Cl(e.element,"rowspan",e.rowspan,1),Ie(t,e.element)})),t})),s=e=>j(e,(e=>E(e.cells,(e=>(Cl(e.element,"span",e.colspan,1),e.element))))),l=(t,o)=>{const n=((e,t)=>{const o=pt(e,t).getOrThunk((()=>{const o=xe.fromTag(t,ke(e).dom);return"thead"===t?Sl(e,"caption,colgroup",o):"colgroup"===t?Sl(e,"caption",o):Ie(e,o),o}));return Ve(o),o})(e,o),l=("colgroup"===o?s:r)(t);$e(n,l)},a=(t,o)=>{t.length>0?l(t,o):(t=>{pt(e,t).each(qe)})(o)},c=[],i=[],m=[],d=[];return N(t,(e=>{switch(e.section){case"thead":c.push(e);break;case"tbody":i.push(e);break;case"tfoot":m.push(e);break;case"colgroup":d.push(e)}})),a(d,"colgroup"),a(c,"thead"),a(i,"tbody"),a(m,"tfoot"),{newRows:o,newCells:n}},Rl=(e,t)=>{if(0===e.length)return 0;const o=e[0];return W(e,(e=>!t(o.element,e.element))).getOr(e.length)},Dl=(e,t)=>{const o=E(e,(e=>E(e.cells,y)));return E(e,((n,r)=>{const s=j(n.cells,((n,s)=>{if(!1===o[r][s]){const m=((e,t,o,n)=>{const r=((e,t)=>e[t])(e,t),s="colgroup"===r.section,l=Rl(r.cells.slice(o),n),a=s?1:Rl(((e,t)=>E(e,(e=>Fo(e,t))))(e.slice(t),o),n);return{colspan:l,rowspan:a}})(e,r,s,t);return((e,t,n,r)=>{for(let s=e;s({element:e,cells:t,section:o,isNew:n}))(n.element,s,n.section,n.isNew)}))},Ol=(e,t,o)=>{const n=[];N(e.colgroups,(r=>{const s=[];for(let n=0;net(e.element,o,!1))).getOrThunk((()=>et(t.colGap(),!0,!1)));s.push(r)}n.push(tt(r.element,s,"colgroup",o))}));for(let r=0;ret(e.element,o,e.isLocked))).getOrThunk((()=>et(t.gap(),!0,!1)));s.push(l)}const l=e.all[r],a=tt(l.element,s,l.section,o);n.push(a)}return n},kl=e=>Dl(e,Re),El=(e,t)=>V(e.all,(e=>L(e.cells,(e=>Re(t,e.element))))),Nl=(e,t,o)=>{const n=E(t.selection,(t=>Vt(t).bind((t=>El(e,t))).filter(o))),r=yt(n);return xt(r.length>0,r)},_l=(e,t,o,n,r)=>(s,l,a,c)=>{const i=Xo(s),m=C.from(null==c?void 0:c.section).getOrThunk(xl);return t(i,l).map((t=>{const o=((e,t)=>Ol(e,t,!1))(i,a),n=e(o,t,Re,r(a),m),s=Ko(n.grid);return{info:t,grid:kl(n.grid),cursor:n.cursor,lockedColumns:s}})).bind((e=>{const t=Tl(s,e.grid),r=C.from(null==c?void 0:c.sizing).getOrThunk((()=>mr(s))),l=C.from(null==c?void 0:c.resize).getOrThunk(ll);return o(s,e.grid,e.info,{sizing:r,resize:l,section:m}),n(s),we(s,Uo),e.lockedColumns.length>0&&ge(s,Uo,e.lockedColumns.join(",")),C.some({cursor:e.cursor,newRows:t.newRows,newCells:t.newCells})}))},Bl=(e,t)=>Nl(e,t,x).map((e=>({cells:e,generators:t.generators,clipboard:t.clipboard}))),zl=(e,t)=>Nl(e,t,x),Al=(e,t)=>Nl(e,t,(e=>!e.isLocked)),Ll=(e,t)=>P(t,(t=>((e,t)=>El(e,t).exists((e=>!e.isLocked)))(e,t))),Wl=(e,t,o,n)=>{const r=Vo(e).rows;let s=!0;for(let e=0;e{const t=t=>t(e),o=g(e),n=()=>r,r={tag:!0,inner:e,fold:(t,o)=>o(e),isValue:x,isError:y,map:t=>Pl.value(t(e)),mapError:n,bind:t,exists:t,forall:t,getOr:o,or:n,getOrThunk:o,orThunk:n,getOrDie:o,each:t=>{t(e)},toOptional:()=>C.some(e)};return r},jl=e=>{const t=()=>o,o={tag:!1,inner:e,fold:(t,o)=>t(e),isValue:y,isError:x,map:t,mapError:t=>Pl.error(t(e)),bind:t,exists:y,forall:x,getOr:h,or:h,getOrThunk:v,orThunk:v,getOrDie:(n=String(e),()=>{throw new Error(n)}),each:f,toOptional:C.none};var n;return o},Pl={value:Ml,error:jl,fromOption:(e,t)=>e.fold((()=>jl(t)),Ml)},Il=(e,t)=>({rowDelta:0,colDelta:$o(e[0])-$o(t[0])}),Fl=(e,t)=>({rowDelta:e.length-t.length,colDelta:0}),Hl=(e,t,o,n)=>{const r="colgroup"===t.section?o.col:o.cell;return k(e,(e=>et(r(),!0,n(e))))},$l=(e,t,o,n)=>{const r=e[e.length-1];return e.concat(k(t,(()=>{const e="colgroup"===r.section?o.colgroup:o.row,t=qo(r,e,h),s=Hl(t.cells.length,t,o,(e=>X(n,e.toString())));return Io(t,s)})))},Vl=(e,t,o,n)=>E(e,(e=>{const r=Hl(t,e,o,y);return Mo(e,n,r)})),ql=(e,t,o)=>{const n=t.colDelta<0?Vl:h,r=t.rowDelta<0?$l:h,s=Ko(e),l=$o(e[0]),a=O(s,(e=>e===l-1)),c=n(e,Math.abs(t.colDelta),o,a?l-1:l),i=Ko(c);return r(c,Math.abs(t.rowDelta),o,I(i,x))},Ul=(e,t,o,n)=>{const r=b(n,Fo(e[t],o).element),s=e[t];return e.length>1&&$o(s)>1&&(o>0&&r(Ho(s,o-1))||o0&&r(Ho(e[t-1],o))||tB(o,(o=>o>=e.column&&o<=$o(t[0])+e.column)),Kl=(e,t,o,n,r)=>{((e,t,o,n)=>{t>0&&t{const r=e.cells[t-1];let s=0;const l=n();for(;e.cells.length>t+s&&o(r.element,e.cells[t+s].element);)Po(e,t+s,et(l,!0,e.cells[t+s].isLocked)),s++}))})(t,e,r,n.cell);const s=Fl(o,t),l=ql(o,s,n),a=Fl(t,l),c=ql(t,a,n);return E(c,((t,o)=>Mo(t,e,l[o].cells)))},Yl=(e,t,o,n,r)=>{((e,t,o,n)=>{const r=Vo(e).rows;if(t>0&&tA(e,((e,o)=>O(e,(e=>t(e.element,o.element)))?e:e.concat([o])),[]))(r[t-1].cells,o);N(e,(e=>{let s=C.none();for(let l=t;l{Po(a,t,et(e,!0,c.isLocked))})))}}))}})(t,e,r,n.cell);const s=Ko(t),l=Il(t,o),a={...l,colDelta:l.colDelta-s.length},c=ql(t,a,n),{cols:i,rows:m}=Vo(c),d=Ko(c),u=Il(o,t),f={...u,colDelta:u.colDelta+d.length},g=(p=n,b=d,E(o,(e=>A(b,((t,o)=>{const n=Hl(1,e,p,x)[0];return jo(t,o,n)}),e)))),h=ql(g,f,n);var p,b;return[...i,...m.slice(0,e),...h,...m.slice(e,m.length)]},Jl=(e,t,o,n,r)=>{const{rows:s,cols:l}=Vo(e),a=s.slice(0,t),c=s.slice(t);return[...l,...a,((e,t,o,n)=>qo(e,(e=>n(e,o)),t))(s[o],((e,o)=>t>0&&tE(e,(e=>{const s=t>0&&t<$o(e)&&n(Ho(e,t-1),Ho(e,t)),l=((e,t,o,n,r,s,l)=>{if("colgroup"!==o&&n)return Fo(e,t);{const t=Fo(e,r);return et(l(t.element,s),!0,!1)}})(e,t,e.section,s,o,n,r);return jo(e,t,l)})),Xl=(e,t,o,n)=>((e,t,o,n)=>void 0!==Ho(e[t],o)&&t>0&&n(Ho(e[t-1],o),Ho(e[t],o)))(e,t,o,n)||((e,t,o)=>t>0&&o(Ho(e,t-1),Ho(e,t)))(e[t],o,n),Zl=(e,t,o,n)=>{const r=e=>(e=>"row"===e?(e=>Mt(e,"rowspan")>1)(t):jt(t))(e)?`${e}group`:e;return e?cl(t)?r(o):null:n&&cl(t)?r("row"===o?"col":"row"):null},ea=(e,t,o)=>et(o(e.element,t),!0,e.isLocked),ta=(e,t,o,n,r,s,l)=>E(e,((e,a)=>(e=>{const c=e.cells,i=E(c,((e,c)=>{if((e=>O(t,(t=>o(e.element,t.element))))(e)){const t=l(e,a,c)?r(e,o,n):e;return s(t,a,c).each((e=>{var o,n;o=t.element,n={scope:C.from(e)},G(n,((e,t)=>{e.fold((()=>{we(o,t)}),(e=>{fe(o.dom,t,e)}))}))})),t}return e}));return tt(e.element,i,e.section,e.isNew)})(e))),oa=(e,t,o)=>j(e,((n,r)=>Xl(e,r,t,o)?[]:[Fo(n,t)])),na=(e,t,o,n,r)=>{const s=Vo(e).rows,l=j(t,(e=>oa(s,e,n))),a=E(s,(e=>il(e.cells))),c=((e,t)=>P(t,h)&&il(e)?x:(e,o,n)=>!("th"===ne(e.element)&&t[o]))(l,a),i=((e,t)=>(o,n)=>C.some(Zl(e,o.element,"row",t[n])))(o,a);return ta(e,l,n,r,ea,i,c)},ra=(e,t,o,n)=>{const r=Vo(e).rows,s=E(t,(e=>Fo(r[e.row],e.column)));return ta(e,s,o,n,ea,C.none,x)},sa=e=>{if(!l(e))throw new Error("cases must be an array");if(0===e.length)throw new Error("there must be at least one case");const t=[],o={};return N(e,((n,r)=>{const s=q(n);if(1!==s.length)throw new Error("one and only one name per case");const a=s[0],c=n[a];if(void 0!==o[a])throw new Error("duplicate key detected:"+a);if("cata"===a)throw new Error("cannot have a case named cata (sorry)");if(!l(c))throw new Error("case arguments must be an array");t.push(a),o[a]=(...o)=>{const n=o.length;if(n!==c.length)throw new Error("Wrong number of arguments to case "+a+". Expected "+c.length+" ("+c+"), got "+n);return{fold:(...t)=>{if(t.length!==e.length)throw new Error("Wrong number of arguments to fold. Expected "+e.length+", got "+t.length);return t[r].apply(null,o)},match:e=>{const n=q(e);if(t.length!==n.length)throw new Error("Wrong number of arguments to match. Expected: "+t.join(",")+"\nActual: "+n.join(","));if(!P(t,(e=>D(n,e))))throw new Error("Not all branches were specified when using match. Specified: "+n.join(", ")+"\nRequired: "+t.join(", "));return e[a].apply(null,o)},log:e=>{console.log(e,{constructors:t,constructor:a,params:o})}}}})),o},la={...sa([{none:[]},{only:["index"]},{left:["index","next"]},{middle:["prev","index","next"]},{right:["prev","index"]}])},aa=(e,t,o)=>{const n=((e,t)=>sn(e)?((e,t)=>{const o=rn(e);return E(o,((e,o)=>({element:e.element,width:t[o],colspan:e.colspan})))})(e,t):((e,t)=>{const o=nn(e);return E(o,(e=>{const o=((e,t,o)=>{let n=0;for(let r=e;r{o.setElementWidth(e.element,e.width)}))},ca=(e,t,o,n,r)=>{const s=Xo(e),l=r.getCellDelta(t),a=r.getWidths(s,r),c=o===s.grid.columns-1,i=n.clampTableDelta(a,o,l,r.minCellWidth(),c),m=((e,t,o,n,r)=>{const s=e.slice(0),l=((e,t)=>0===e.length?la.none():1===e.length?la.only(0):0===t?la.left(0,1):t===e.length-1?la.right(t-1,t):t>0&&tn.singleColumnWidth(s[e],o)),((e,t)=>r.calcLeftEdgeDeltas(s,e,t,o,n.minCellWidth(),n.isRelative)),((e,t,l)=>r.calcMiddleDeltas(s,e,t,l,o,n.minCellWidth(),n.isRelative)),((e,t)=>r.calcRightEdgeDeltas(s,e,t,o,n.minCellWidth(),n.isRelative)))})(a,o,i,r,n),d=E(m,((e,t)=>e+a[t]));aa(s,d,r),n.resizeTable(r.adjustTableWidth,i,c)},ia=(e,t,o)=>{const n=Xo(e),r=((e,t)=>lr(e,t,Yn,(e=>e.getOrThunk(Ft))))(n,e),s=E(r,((e,n)=>o===n?Math.max(t+e,Ft()):e)),l=((e,t)=>E(e.all,((e,o)=>({element:e.element,height:t[o]}))))(n,s);N(l,(e=>{$n(e.element,e.height)})),N(nn(n),(e=>{(e=>{Lt(e,"height")})(e.element)}));const a=z(s,((e,t)=>e+t),0);$n(e,a)},ma=e=>A(e,((e,t)=>O(e,(e=>e.column===t.column))?e:e.concat([t])),[]).sort(((e,t)=>e.column-t.column)),da=ue("col"),ua=ue("colgroup"),fa=e=>"tr"===ne(e)||ua(e),ga=e=>({element:e,colspan:Wt(e,"colspan",1),rowspan:Wt(e,"rowspan",1)}),ha=e=>be(e,"scope").map((e=>e.substr(0,3))),pa=(e,t=ga)=>{const o=o=>{if(fa(o))return ua((r={element:o}).element)?e.colgroup(r):e.row(r);{const r=o,s=(t=>da(t.element)?e.col(t):e.cell(t))(t(r));return n=C.some({item:r,replacement:s}),s}var r};let n=C.none();return{getOrInit:(e,t)=>n.fold((()=>o(e)),(n=>t(e,n.item)?n.replacement:o(e)))}},ba=e=>t=>{const o=[],n=n=>{const r="td"===e?{scope:null}:{},s=t.replace(n,e,r);return o.push({item:n,sub:s}),s};return{replaceOrInit:(e,t)=>{if(fa(e)||da(e))return e;{const r=e;return((e,t)=>L(o,(o=>t(o.item,e))))(r,t).fold((()=>n(r)),(o=>t(e,o.item)?o.sub:n(r)))}}}},wa=e=>({unmerge:t=>{const o=ha(t);return o.each((e=>ge(t,"scope",e))),()=>{const n=e.cell({element:t,colspan:1,rowspan:1});return Lt(n,"width"),Lt(t,"width"),o.each((e=>ge(n,"scope",e))),n}},merge:e=>(Lt(e[0],"width"),(()=>{const t=yt(E(e,ha));if(0===t.length)return C.none();{const e=t[0],o=["row","col"];return O(t,(t=>t!==e&&D(o,t)))?C.none():C.from(e)}})().fold((()=>we(e[0],"scope")),(t=>ge(e[0],"scope",t+"group"))),g(e[0]))}),va=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","table","thead","tfoot","tbody","caption","tr","td","th","h1","h2","h3","h4","h5","h6","blockquote","pre","address"],ya=xs(),xa=e=>((e,t)=>{const o=e.property().name(t);return D(va,o)})(ya,e),Ca=e=>((e,t)=>{const o=e.property().name(t);return D(["ol","ul"],o)})(ya,e),Sa=e=>{const t=ue("br"),o=e=>Cr(e).bind((o=>{const n=Ae(o).map((e=>!!xa(e)||!!((e,t)=>D(["br","img","hr","input"],e.property().name(t)))(ya,e)&&"img"!==ne(e))).getOr(!1);return Ne(o).map((r=>{return!0===n||("li"===ne(s=r)||ft(s,Ca).isSome())||t(o)||xa(r)&&!Re(e,r)?[]:[xe.fromTag("br")];var s}))})).getOr([]),n=(()=>{const n=j(e,(e=>{const n=Le(e);return(e=>P(e,(e=>t(e)||ie(e)&&0===hr(e).trim().length)))(n)?[]:n.concat(o(e))}));return 0===n.length?[xe.fromTag("br")]:n})();Ve(e[0]),$e(e[0],n)},Ta=e=>es(e,!0),Ra=e=>{0===qt(e).length&&qe(e)},Da=(e,t)=>({grid:e,cursor:t}),Oa=(e,t,o)=>{const n=((e,t,o)=>{var n,r;const s=Vo(e).rows;return C.from(null===(r=null===(n=s[t])||void 0===n?void 0:n.cells[o])||void 0===r?void 0:r.element).filter(Ta).orThunk((()=>(e=>V(e,(e=>V(e.cells,(e=>{const t=e.element;return xt(Ta(t),t)})))))(s)))})(e,t,o);return Da(e,n)},ka=e=>A(e,((e,t)=>O(e,(e=>e.row===t.row))?e:e.concat([t])),[]).sort(((e,t)=>e.row-t.row)),Ea=(e,t)=>(o,n,r,s,l)=>{const a=ka(n),c=E(a,(e=>e.row)),i=((e,t,o,n,r,s,l)=>{const{cols:a,rows:c}=Vo(e),i=c[t[0]],m=j(t,(e=>((e,t,o)=>{const n=e[t];return j(n.cells,((n,r)=>Xl(e,t,r,o)?[]:[n]))})(c,e,r))),d=E(i.cells,((e,t)=>il(oa(c,t,r)))),u=[...c];N(t,(e=>{u[e]=l.transformRow(c[e],o)}));const f=[...a,...u],g=((e,t)=>P(t,h)&&il(e.cells)?x:(e,o,n)=>!("th"===ne(e.element)&&t[n]))(i,d),p=((e,t)=>(o,n,r)=>C.some(Zl(e,o.element,"col",t[r])))(n,d);return ta(f,m,r,s,l.transformCell,p,g)})(o,c,e,t,r,s.replaceOrInit,l);return Oa(i,n[0].row,n[0].column)},Na=Ea("thead",!0),_a=Ea("tbody",!1),Ba=Ea("tfoot",!1),za=(e,t,o)=>{const n=((e,t)=>Jt(e,(()=>t)))(e,o.section),r=Zo(n);return Ol(r,t,!0)},Aa=(e,t,o,n)=>((e,t,o,n)=>{const r=Zo(t),s=n.getWidths(r,n);aa(r,s,n)})(0,t,0,n.sizing),La=(e,t,o,n)=>((e,t,o,n,r)=>{const s=Zo(t),l=n.getWidths(s,n),a=n.pixelWidth(),{newSizes:c,delta:i}=r.calcRedestributedWidths(l,a,o.pixelDelta,n.isRelative);aa(s,c,n),n.adjustTableWidth(i)})(0,t,o,n.sizing,n.resize),Wa=(e,t)=>O(t,(e=>0===e.column&&e.isLocked)),Ma=(e,t)=>O(t,(t=>t.column+t.colspan>=e.grid.columns&&t.isLocked)),ja=(e,t)=>{const o=an(e),n=ma(t);return A(n,((e,t)=>e+o[t.column].map(Lo).getOr(0)),0)},Pa=e=>(t,o)=>zl(t,o).filter((o=>!(e?Wa:Ma)(t,o))).map((e=>({details:e,pixelDelta:ja(t,e)}))),Ia=e=>(t,o)=>Bl(t,o).filter((o=>!(e?Wa:Ma)(t,o.cells))),Fa=ba("th"),Ha=ba("td"),$a=_l(((e,t,o,n)=>{const r=t[0].row,s=ka(t),l=z(s,((e,t)=>({grid:Jl(e.grid,r,t.row+e.delta,o,n.getOrInit),delta:e.delta+1})),{grid:e,delta:0}).grid;return Oa(l,r,t[0].column)}),zl,f,f,pa),Va=_l(((e,t,o,n)=>{const r=ka(t),s=r[r.length-1],l=s.row+s.rowspan,a=z(r,((e,t)=>Jl(e,l,t.row,o,n.getOrInit)),e);return Oa(a,l,t[0].column)}),zl,f,f,pa),qa=_l(((e,t,o,n)=>{const r=t.details,s=ma(r),l=s[0].column,a=z(s,((e,t)=>({grid:Ql(e.grid,l,t.column+e.delta,o,n.getOrInit),delta:e.delta+1})),{grid:e,delta:0}).grid;return Oa(a,r[0].row,l)}),Pa(!0),La,f,pa),Ua=_l(((e,t,o,n)=>{const r=t.details,s=r[r.length-1],l=s.column+s.colspan,a=ma(r),c=z(a,((e,t)=>Ql(e,l,t.column,o,n.getOrInit)),e);return Oa(c,r[0].row,l)}),Pa(!1),La,f,pa),Ga=_l(((e,t,o,n)=>{const r=ma(t.details),s=((e,t)=>j(e,(e=>{const o=e.cells,n=z(t,((e,t)=>t>=0&&t0?[tt(e.element,n,e.section,e.isNew)]:[]})))(e,E(r,(e=>e.column))),l=s.length>0?s[0].cells.length-1:0;return Oa(s,r[0].row,Math.min(r[0].column,l))}),((e,t)=>Al(e,t).map((t=>({details:t,pixelDelta:-ja(e,t)})))),La,Ra,pa),Ka=_l(((e,t,o,n)=>{const r=ka(t),s=((e,t,o)=>{const{rows:n,cols:r}=Vo(e);return[...r,...n.slice(0,t),...n.slice(o+1)]})(e,r[0].row,r[r.length-1].row),l=Math.max(Vo(s).rows.length-1,0);return Oa(s,Math.min(t[0].row,l),t[0].column)}),zl,f,Ra,pa),Ya=_l(((e,t,o,n)=>{const r=ma(t),s=E(r,(e=>e.column)),l=na(e,s,!0,o,n.replaceOrInit);return Oa(l,t[0].row,t[0].column)}),Al,f,f,Fa),Ja=_l(((e,t,o,n)=>{const r=ma(t),s=E(r,(e=>e.column)),l=na(e,s,!1,o,n.replaceOrInit);return Oa(l,t[0].row,t[0].column)}),Al,f,f,Ha),Qa=_l(Na,zl,f,f,Fa),Xa=_l(_a,zl,f,f,Ha),Za=_l(Ba,zl,f,f,Ha),ec=_l(((e,t,o,n)=>{const r=ra(e,t,o,n.replaceOrInit);return Oa(r,t[0].row,t[0].column)}),Al,f,f,Fa),tc=_l(((e,t,o,n)=>{const r=ra(e,t,o,n.replaceOrInit);return Oa(r,t[0].row,t[0].column)}),Al,f,f,Ha),oc=_l(((e,t,o,n)=>{const r=t.cells;Sa(r);const s=((e,t,o,n)=>{const r=Vo(e).rows;if(0===r.length)return e;for(let e=t.startRow;e<=t.finishRow;e++)for(let o=t.startCol;o<=t.finishCol;o++){const t=r[e],s=Fo(t,o).isLocked;Po(t,o,et(n(),!1,s))}return e})(e,t.bounds,0,n.merge(r));return Da(s,C.from(r[0]))}),((e,t)=>((e,t)=>t.mergable)(0,t).filter((t=>Ll(e,t.cells)))),Aa,f,wa),nc=_l(((e,t,o,n)=>{const r=z(t,((e,t)=>Wl(e,t,o,n.unmerge(t))),e);return Da(r,C.from(t[0]))}),((e,t)=>((e,t)=>t.unmergable)(0,t).filter((t=>Ll(e,t)))),Aa,f,wa),rc=_l(((e,t,o,n)=>{const r=((e,t)=>{const o=Xo(e);return Ol(o,t,!0)})(t.clipboard,t.generators);var s,l;return((e,t,o,n,r)=>{const s=Ko(t),l=((e,t,o)=>{const n=$o(t[0]),r=Vo(t).cols.length+e.row,s=k(n-e.column,(t=>t+e.column));return{row:r,column:L(s,(e=>P(o,(t=>t!==e)))).getOr(n-1)}})(e,t,s),a=Vo(o).rows,c=Gl(l,a,s),i=((e,t,o)=>{if(e.row>=t.length||e.column>$o(t[0]))return Pl.error("invalid start address out of table bounds, row: "+e.row+", column: "+e.column);const n=t.slice(e.row),r=n[0].cells.slice(e.column),s=$o(o[0]),l=o.length;return Pl.value({rowDelta:n.length-l,colDelta:r.length-s})})(l,t,a);return i.map((e=>{const o={...e,colDelta:e.colDelta-c.length},s=ql(t,o,n),i=Ko(s),m=Gl(l,a,i);return((e,t,o,n,r,s)=>{const l=e.row,a=e.column,c=l+o.length,i=a+$o(o[0])+s.length,m=I(s,x);for(let e=l;eDa(e,C.some(t.element))),(e=>Oa(e,t.row,t.column)))}),((e,t)=>Vt(t.element).bind((o=>El(e,o).map((e=>({...e,generators:t.generators,clipboard:t.clipboard})))))),Aa,f,pa),sc=_l(((e,t,o,n)=>{const r=Vo(e).rows,s=t.cells[0].column,l=r[t.cells[0].row],a=za(t.clipboard,t.generators,l),c=Kl(s,e,a,t.generators,o);return Oa(c,t.cells[0].row,t.cells[0].column)}),Ia(!0),f,f,pa),lc=_l(((e,t,o,n)=>{const r=Vo(e).rows,s=t.cells[t.cells.length-1].column+t.cells[t.cells.length-1].colspan,l=r[t.cells[0].row],a=za(t.clipboard,t.generators,l),c=Kl(s,e,a,t.generators,o);return Oa(c,t.cells[0].row,s)}),Ia(!1),f,f,pa),ac=_l(((e,t,o,n)=>{const r=Vo(e).rows,s=t.cells[0].row,l=r[s],a=za(t.clipboard,t.generators,l),c=Yl(s,e,a,t.generators,o);return Oa(c,t.cells[0].row,t.cells[0].column)}),Bl,f,f,pa),cc=_l(((e,t,o,n)=>{const r=Vo(e).rows,s=t.cells[t.cells.length-1].row+t.cells[t.cells.length-1].rowspan,l=r[t.cells[0].row],a=za(t.clipboard,t.generators,l),c=Yl(s,e,a,t.generators,o);return Oa(c,s,t.cells[0].column)}),Bl,f,f,pa),ic=(e,t)=>{const o=Xo(e);return zl(o,t).bind((e=>{const t=e[e.length-1],n=e[0].column,r=t.column+t.colspan,s=M(E(o.all,(e=>B(e.cells,(e=>e.column>=n&&e.column{const o=Xo(e);return zl(o,t).bind(ul).getOr("")},dc=(e,t)=>{const o=Xo(e);return zl(o,t).bind((e=>{const t=e[e.length-1],n=e[0].row,r=t.row+t.rowspan;return(e=>{const t=E(e,(e=>dl(e).type)),o=D(t,"header"),n=D(t,"footer");if(o||n){const e=D(t,"body");return!o||e||n?o||e||!n?C.none():C.some("footer"):C.some("header")}return C.some("body")})(o.all.slice(n,r))})).getOr("")},uc=(e,t)=>e.dispatch("NewRow",{node:t}),fc=(e,t)=>e.dispatch("NewCell",{node:t}),gc=(e,t,o)=>{e.dispatch("TableModified",{...o,table:t})},hc={structure:!1,style:!0},pc={structure:!0,style:!1},bc={structure:!0,style:!0},wc=(e,t)=>Hr(e)?ur(t):$r(e)?dr(t):mr(t),vc=(e,t,o)=>{const n=e=>"table"===ne(os(e)),r=Wr(e),s=Ir(e)?f:Zs,l=t=>{switch(Mr(e)){case"section":return wl();case"sectionCells":return vl();case"cells":return yl();default:return((e,t)=>{var o;switch((o=Xo(e),V(o.all,(e=>{const t=dl(e);return"header"===t.type?C.from(t.subType):C.none()}))).getOr(t)){case"section":return hl();case"sectionCells":return pl();case"cells":return bl()}})(t,"section")}},a=(n,s,a,c)=>(i,m,d=!1)=>{rs(i);const u=xe.fromDom(e.getDoc()),f=_r(a,u,r),g={sizing:wc(e,i),resize:Ir(e)?sl():ll(),section:l(i)};return s(i)?n(i,m,f,g).bind((n=>{t.refresh(i.dom),N(n.newRows,(t=>{uc(e,t.dom)})),N(n.newCells,(t=>{fc(e,t.dom)}));const r=((t,n)=>n.cursor.fold((()=>{const n=qt(t);return H(n).filter(st).map((n=>{o.clearSelectedCells(t.dom);const r=e.dom.createRng();return r.selectNode(n.dom),e.selection.setRng(r),ge(n,"data-mce-selected","1"),r}))}),(n=>{const r=Qs(Xs,n),s=e.dom.createRng();return s.setStart(r.element.dom,r.offset),s.setEnd(r.element.dom,r.offset),e.selection.setRng(s),o.clearSelectedCells(t.dom),C.some(s)})))(i,n);return st(i)&&(rs(i),d||gc(e,i.dom,c)),r.map((e=>({rng:e,effect:c})))})):C.none()},c=a(Ka,(t=>!n(e)||al(t).rows>1),f,pc),i=a(Ga,(t=>!n(e)||al(t).columns>1),f,pc);return{deleteRow:c,deleteColumn:i,insertRowsBefore:a($a,x,f,pc),insertRowsAfter:a(Va,x,f,pc),insertColumnsBefore:a(qa,x,s,pc),insertColumnsAfter:a(Ua,x,s,pc),mergeCells:a(oc,x,f,pc),unmergeCells:a(nc,x,f,pc),pasteColsBefore:a(sc,x,f,pc),pasteColsAfter:a(lc,x,f,pc),pasteRowsBefore:a(ac,x,f,pc),pasteRowsAfter:a(cc,x,f,pc),pasteCells:a(rc,x,f,bc),makeCellsHeader:a(ec,x,f,pc),unmakeCellsHeader:a(tc,x,f,pc),makeColumnsHeader:a(Ya,x,f,pc),unmakeColumnsHeader:a(Ja,x,f,pc),makeRowsHeader:a(Qa,x,f,pc),makeRowsBody:a(Xa,x,f,pc),makeRowsFooter:a(Za,x,f,pc),getTableRowType:dc,getTableCellType:mc,getTableColType:ic}},yc=(e,t,o)=>{const n=Wt(e,t,1);1===o||n<=1?we(e,t):ge(e,t,Math.min(o,n))},xc=(e,t)=>o=>{const n=o.column+o.colspan-1,r=o.column;return n>=e&&r{const n=o.substring(0,o.length-e.length),r=parseFloat(n);return n===r.toString()?t(r):Cc.invalid(o)},Tc={...Cc,from:e=>Rt(e,"%")?Sc("%",Cc.percent,e):Rt(e,"px")?Sc("px",Cc.pixels,e):Cc.invalid(e)},Rc=(e,t,o)=>{const n=Tc.from(o),r=P(e,(e=>"0px"===e))?((e,t)=>{const o=e.fold((()=>g("")),(e=>g(e/t+"px")),(()=>g(100/t+"%")));return k(t,o)})(n,e.length):((e,t,o)=>e.fold((()=>t),(e=>((e,t,o)=>{const n=o/t;return E(e,(e=>Tc.from(e).fold((()=>e),(e=>e*n+"px"),(e=>e/100*o+"px"))))})(t,o,e)),(e=>((e,t)=>E(e,(e=>Tc.from(e).fold((()=>e),(e=>e/t*100+"%"),(e=>e+"%")))))(t,o))))(n,e,t);return kc(r)},Dc=(e,t)=>0===e.length?t:z(e,((e,t)=>Tc.from(t).fold(g(0),h,h)+e),0),Oc=(e,t)=>Tc.from(e).fold(g(e),(e=>e+t+"px"),(e=>e+t+"%")),kc=e=>{if(0===e.length)return e;const t=z(e,((e,t)=>{const o=Tc.from(t).fold((()=>({value:t,remainder:0})),(e=>(e=>{const t=Math.floor(e);return{value:t+"px",remainder:e-t}})(e)),(e=>({value:e+"%",remainder:0})));return{output:[o.value].concat(e.output),remainder:e.remainder+o.remainder}}),{output:[],remainder:0}),o=t.output;return o.slice(0,o.length-1).concat([Oc(o[o.length-1],Math.round(t.remainder))])},Ec=Tc.from,Nc=(e,t,o)=>{const n=Xo(e),r=n.all,s=nn(n),l=rn(n);t.each((t=>{const o=Ec(t).fold(g("px"),g("px"),g("%")),r=Ao(e),a=((e,t)=>nr(e,t,er,rr))(n,e),c=Rc(a,r,t);sn(n)?((e,t,o)=>{N(t,((t,n)=>{const r=Dc([e[n]],It());Nt(t.element,"width",r+o)}))})(c,l,o):((e,t,o)=>{N(t,(t=>{const n=e.slice(t.column,t.colspan+t.column),r=Dc(n,It());Nt(t.element,"width",r+o)}))})(c,s,o),Nt(e,"width",t)})),o.each((t=>{const o=gn(e),l=((e,t)=>lr(e,t,tr,rr))(n,e);((e,t,o)=>{N(o,(e=>{Lt(e.element,"height")})),N(t,((t,o)=>{Nt(t.element,"height",e[o])}))})(Rc(l,o,t),r,s),Nt(e,"height",t)}))},_c=e=>Un(e).exists((e=>Wn.test(e))),Bc=e=>Un(e).exists((e=>Mn.test(e))),zc=e=>Un(e).isNone(),Ac=e=>{we(e,"width"),we(e,"height")},Lc=e=>{const t=Qn(e);Nc(e,C.some(t),C.none()),Ac(e)},Wc=e=>{const t=(e=>Ao(e)+"px")(e);Nc(e,C.some(t),C.none()),Ac(e)},Mc=e=>{Lt(e,"width");const t=Ut(e),o=t.length>0?t:qt(e);N(o,(e=>{Lt(e,"width"),Ac(e)})),Ac(e)},jc={styles:{"border-collapse":"collapse",width:"100%"},attributes:{border:"1"},colGroups:!1},Pc=(e,t,o,n)=>k(e,(e=>((e,t,o,n)=>{const r=xe.fromTag("tr");for(let s=0;s{e.selection.select(t.dom,!0),e.selection.collapse(!0)},Fc=(e,t,o,n,s)=>{const l=(e=>{const t=e.options,o=t.get("table_default_styles");return t.isSet("table_default_styles")?o:((e,t)=>Vr(e)||!Ur(e)?t:$r(e)?{...t,width:Lr(e)}:{...t,width:Ar})(e,o)})(e),a={styles:l,attributes:Kr(e),colGroups:Yr(e)};return e.undoManager.ignore((()=>{const r=((e,t,o,n,r,s=jc)=>{const l=xe.fromTag("table"),a="cells"!==r;_t(l,s.styles),he(l,s.attributes),s.colGroups&&Ie(l,(e=>{const t=xe.fromTag("colgroup");return k(e,(()=>Ie(t,xe.fromTag("col")))),t})(t));const c=Math.min(e,o);if(a&&o>0){const e=xe.fromTag("thead");Ie(l,e);const s=Pc(o,t,"sectionCells"===r?c:0,n);$e(e,s)}const i=xe.fromTag("tbody");Ie(l,i);const m=Pc(a?e-c:e,t,a?0:o,n);return $e(i,m),l})(o,t,s,n,Mr(e),a);ge(r,"data-mce-id","__mce");const l=(e=>{const t=xe.fromTag("div"),o=xe.fromDom(e.dom.cloneNode(!0));return Ie(t,o),(e=>e.dom.innerHTML)(t)})(r);e.insertContent(l),e.addVisual()})),bt(os(e),'table[data-mce-id="__mce"]').map((t=>($r(e)?Wc(t):Vr(e)?Mc(t):(Hr(e)||(e=>r(e)&&-1!==e.indexOf("%"))(l.width))&&Lc(t),rs(t),we(t,"data-mce-id"),((e,t)=>{N(dt(t,"tr"),(t=>{uc(e,t.dom),N(dt(t,"th,td"),(t=>{fc(e,t.dom)}))}))})(e,t),((e,t)=>{bt(t,"td,th").each(b(Ic,e))})(e,t),t.dom))).getOrNull()};var Hc=tinymce.util.Tools.resolve("tinymce.FakeClipboard");const $c="x-tinymce/dom-table-",Vc=$c+"rows",qc=$c+"columns",Uc=e=>{const t=Hc.FakeClipboardItem(e);Hc.write([t])},Gc=e=>{var t;const o=null!==(t=Hc.read())&&void 0!==t?t:[];return V(o,(t=>C.from(t.getType(e))))},Kc=e=>{Gc(e).isSome()&&Hc.clear()},Yc=e=>{e.fold(Qc,(e=>Uc({[Vc]:e})))},Jc=()=>Gc(Vc),Qc=()=>Kc(Vc),Xc=e=>{e.fold(ei,(e=>Uc({[qc]:e})))},Zc=()=>Gc(qc),ei=()=>Kc(qc),ti=e=>$s(ss(e),ns(e)).filter(ds),oi=(e,t)=>{const o=ns(e),n=e=>Gt(e,o),l=t=>(e=>Vs(ss(e),ns(e)).filter(ds))(e).bind((e=>n(e).map((o=>t(o,e))))),a=t=>{e.focus()},c=(t,o=!1)=>l(((n,r)=>{const s=Fs(qs(e),n,r);t(n,s,o).each(a)})),i=()=>l(((t,o)=>((e,t,o)=>{const n=Xo(e);return zl(n,t).bind((e=>{const t=Ol(n,o,!1),r=Vo(t).rows.slice(e[0].row,e[e.length-1].row+e[e.length-1].rowspan),s=j(r,(e=>{const t=B(e.cells,(e=>!e.isLocked));return t.length>0?[{...e,cells:t}]:[]})),l=kl(s);return xt(l.length>0,l)})).map((e=>E(e,(e=>{const t=Ke(e.element);return N(e.cells,(e=>{const o=Ye(e.element);Cl(o,"colspan",e.colspan,1),Cl(o,"rowspan",e.rowspan,1),Ie(t,o)})),t}))))})(t,Fs(qs(e),t,o),_r(f,xe.fromDom(e.getDoc()),C.none())))),m=()=>l(((t,o)=>((e,t)=>{const o=Xo(e);return Al(o,t).map((e=>{const t=e[e.length-1],n=e[0].column,r=t.column+t.colspan,s=((e,t,o)=>{if(sn(e)){const n=B(rn(e),xc(t,o)),r=E(n,(e=>{const n=Ye(e.element);return yc(n,"span",o-t),n})),s=xe.fromTag("colgroup");return $e(s,r),[s]}return[]})(o,n,r),l=((e,t,o)=>E(e.all,(e=>{const n=B(e.cells,xc(t,o)),r=E(n,(e=>{const n=Ye(e.element);return yc(n,"colspan",o-t),n})),s=xe.fromTag("tr");return $e(s,r),s})))(o,n,r);return[...s,...l]}))})(t,Fs(qs(e),t,o)))),d=(t,o)=>o().each((o=>{const n=E(o,(e=>Ye(e)));l(((o,r)=>{const s=Br(xe.fromDom(e.getDoc())),l=((e,t,o,n)=>({selection:zs(e),clipboard:o,generators:n}))(qs(e),0,n,s);t(o,l).each(a)}))})),g=e=>(t,o)=>((e,t)=>X(e,t)?C.from(e[t]):C.none())(o,"type").each((t=>{c(e(t),o.no_events)}));G({mceTableSplitCells:()=>c(t.unmergeCells),mceTableMergeCells:()=>c(t.mergeCells),mceTableInsertRowBefore:()=>c(t.insertRowsBefore),mceTableInsertRowAfter:()=>c(t.insertRowsAfter),mceTableInsertColBefore:()=>c(t.insertColumnsBefore),mceTableInsertColAfter:()=>c(t.insertColumnsAfter),mceTableDeleteCol:()=>c(t.deleteColumn),mceTableDeleteRow:()=>c(t.deleteRow),mceTableCutCol:()=>m().each((e=>{Xc(e),c(t.deleteColumn)})),mceTableCutRow:()=>i().each((e=>{Yc(e),c(t.deleteRow)})),mceTableCopyCol:()=>m().each((e=>Xc(e))),mceTableCopyRow:()=>i().each((e=>Yc(e))),mceTablePasteColBefore:()=>d(t.pasteColsBefore,Zc),mceTablePasteColAfter:()=>d(t.pasteColsAfter,Zc),mceTablePasteRowBefore:()=>d(t.pasteRowsBefore,Jc),mceTablePasteRowAfter:()=>d(t.pasteRowsAfter,Jc),mceTableDelete:()=>ti(e).each((t=>{Gt(t,o).filter(w(o)).each((t=>{const o=xe.fromText("");if(je(t,o),qe(t),e.dom.isEmpty(e.getBody()))e.setContent(""),e.selection.setCursorLocation();else{const t=e.dom.createRng();t.setStart(o.dom,0),t.setEnd(o.dom,0),e.selection.setRng(t),e.nodeChanged()}}))})),mceTableCellToggleClass:(t,o)=>{l((t=>{const n=qs(e),r=P(n,(t=>e.formatter.match("tablecellclass",{value:o},t.dom))),s=r?e.formatter.remove:e.formatter.apply;N(n,(e=>s("tablecellclass",{value:o},e.dom))),gc(e,t.dom,hc)}))},mceTableToggleClass:(t,o)=>{l((t=>{e.formatter.toggle("tableclass",{value:o},t.dom),gc(e,t.dom,hc)}))},mceTableToggleCaption:()=>{ti(e).each((t=>{Gt(t,o).each((o=>{pt(o,"caption").fold((()=>{const t=xe.fromTag("caption");Ie(t,xe.fromText("Caption")),((e,t)=>{We(e,0).fold((()=>{Ie(e,t)}),(e=>{Me(e,t)}))})(o,t),e.selection.setCursorLocation(t.dom,0)}),(n=>{ue("caption")(t)&&Te("td",o).each((t=>e.selection.setCursorLocation(t.dom,0))),qe(n)})),gc(e,o.dom,pc)}))}))},mceTableSizingMode:(t,n)=>(t=>ti(e).each((n=>{Vr(e)||$r(e)||Hr(e)||Gt(n,o).each((o=>{"relative"!==t||_c(o)?"fixed"!==t||Bc(o)?"responsive"!==t||zc(o)||Mc(o):Wc(o):Lc(o),rs(o),gc(e,o.dom,pc)}))})))(n),mceTableCellType:g((e=>"th"===e?t.makeCellsHeader:t.unmakeCellsHeader)),mceTableColType:g((e=>"th"===e?t.makeColumnsHeader:t.unmakeColumnsHeader)),mceTableRowType:g((e=>{switch(e){case"header":return t.makeRowsHeader;case"footer":return t.makeRowsFooter;default:return t.makeRowsBody}}))},((t,o)=>e.addCommand(o,t))),e.addCommand("mceInsertTable",((t,o)=>{((e,t,o,n={})=>{const r=e=>u(e)&&e>0;if(r(t)&&r(o)){const r=n.headerRows||0,s=n.headerColumns||0;return Fc(e,o,t,s,r)}console.error("Invalid values for mceInsertTable - rows and columns values are required to insert a table.")})(e,o.rows,o.columns,o.options)})),e.addCommand("mceTableApplyCellStyle",((t,o)=>{const l=e=>"tablecell"+e.toLowerCase().replace("-","");if(!s(o))return;const a=B(qs(e),ds);if(0===a.length)return;const c=((e,t)=>{const o={};return((e,t,o,n)=>{G(e,((e,r)=>{(t(e,r)?o:n)(e,r)}))})(e,t,(e=>(t,o)=>{e[o]=t})(o),f),o})(o,((t,o)=>e.formatter.has(l(o))&&r(t)));(e=>{for(const t in e)if(U.call(e,t))return!1;return!0})(c)||(G(c,((t,o)=>{const n=l(o);N(a,(o=>{""===t?e.formatter.remove(n,{value:null},o.dom,!0):e.formatter.apply(n,{value:t},o.dom)}))})),n(a[0]).each((t=>gc(e,t.dom,hc))))}))},ni=sa([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),ri={before:ni.before,on:ni.on,after:ni.after,cata:(e,t,o,n)=>e.fold(t,o,n),getStart:e=>e.fold(h,h,h)},si=(e,t)=>({selection:e,kill:t}),li=(e,t)=>{const o=e.document.createRange();return o.selectNode(t.dom),o},ai=(e,t)=>{const o=e.document.createRange();return ci(o,t),o},ci=(e,t)=>e.selectNodeContents(t.dom),ii=(e,t,o)=>{const n=e.document.createRange();var r;return r=n,t.fold((e=>{r.setStartBefore(e.dom)}),((e,t)=>{r.setStart(e.dom,t)}),(e=>{r.setStartAfter(e.dom)})),((e,t)=>{t.fold((t=>{e.setEndBefore(t.dom)}),((t,o)=>{e.setEnd(t.dom,o)}),(t=>{e.setEndAfter(t.dom)}))})(n,o),n},mi=(e,t,o,n,r)=>{const s=e.document.createRange();return s.setStart(t.dom,o),s.setEnd(n.dom,r),s},di=e=>({left:e.left,top:e.top,right:e.right,bottom:e.bottom,width:e.width,height:e.height}),ui=sa([{ltr:["start","soffset","finish","foffset"]},{rtl:["start","soffset","finish","foffset"]}]),fi=(e,t,o)=>t(xe.fromDom(o.startContainer),o.startOffset,xe.fromDom(o.endContainer),o.endOffset),gi=(e,t)=>{const o=((e,t)=>t.match({domRange:e=>({ltr:g(e),rtl:C.none}),relative:(t,o)=>({ltr:Zt((()=>ii(e,t,o))),rtl:Zt((()=>C.some(ii(e,o,t))))}),exact:(t,o,n,r)=>({ltr:Zt((()=>mi(e,t,o,n,r))),rtl:Zt((()=>C.some(mi(e,n,r,t,o))))})}))(e,t);return((e,t)=>{const o=t.ltr();return o.collapsed?t.rtl().filter((e=>!1===e.collapsed)).map((e=>ui.rtl(xe.fromDom(e.endContainer),e.endOffset,xe.fromDom(e.startContainer),e.startOffset))).getOrThunk((()=>fi(0,ui.ltr,o))):fi(0,ui.ltr,o)})(0,o)},hi=(e,t)=>gi(e,t).match({ltr:(t,o,n,r)=>{const s=e.document.createRange();return s.setStart(t.dom,o),s.setEnd(n.dom,r),s},rtl:(t,o,n,r)=>{const s=e.document.createRange();return s.setStart(n.dom,r),s.setEnd(t.dom,o),s}});ui.ltr,ui.rtl;const pi=(e,t,o,n)=>({start:e,soffset:t,finish:o,foffset:n}),bi=(e,t,o,n)=>({start:ri.on(e,t),finish:ri.on(o,n)}),wi=(e,t)=>{const o=hi(e,t);return pi(xe.fromDom(o.startContainer),o.startOffset,xe.fromDom(o.endContainer),o.endOffset)},vi=bi,yi=(e,t,o,n,r)=>Re(o,n)?C.none():Os(o,n,t).bind((t=>{const n=t.boxes.getOr([]);return n.length>1?(r(e,n,t.start,t.finish),C.some(si(C.some(vi(o,0,o,wr(o))),!0))):C.none()})),xi=(e,t)=>({item:e,mode:t}),Ci=(e,t,o,n=Si)=>e.property().parent(t).map((e=>xi(e,n))),Si=(e,t,o,n=Ti)=>o.sibling(e,t).map((e=>xi(e,n))),Ti=(e,t,o,n=Ti)=>{const r=e.property().children(t);return o.first(r).map((e=>xi(e,n)))},Ri=[{current:Ci,next:Si,fallback:C.none()},{current:Si,next:Ti,fallback:C.some(Ci)},{current:Ti,next:Ti,fallback:C.some(Si)}],Di=(e,t,o,n,r=Ri)=>L(r,(e=>e.current===o)).bind((o=>o.current(e,t,n,o.next).orThunk((()=>o.fallback.bind((o=>Di(e,t,o,n))))))),Oi=(e,t,o,n,r,s)=>Di(e,t,n,r).bind((t=>s(t.item)?C.none():o(t.item)?C.some(t.item):Oi(e,t.item,o,t.mode,r,s))),ki=e=>t=>0===e.property().children(t).length,Ei=(e,t,o,n)=>Oi(e,t,o,Si,{sibling:(e,t)=>e.query().prevSibling(t),first:e=>e.length>0?C.some(e[e.length-1]):C.none()},n),Ni=(e,t,o,n)=>Oi(e,t,o,Si,{sibling:(e,t)=>e.query().nextSibling(t),first:e=>e.length>0?C.some(e[0]):C.none()},n),_i=xs(),Bi=(e,t)=>((e,t,o)=>Ei(e,t,ki(e),o))(_i,e,t),zi=(e,t)=>((e,t,o)=>Ni(e,t,ki(e),o))(_i,e,t),Ai=sa([{none:["message"]},{success:[]},{failedUp:["cell"]},{failedDown:["cell"]}]),Li=e=>wt(e,"tr"),Wi={...Ai,verify:(e,t,o,n,r,s,l)=>wt(n,"td,th",l).bind((o=>wt(t,"td,th",l).map((t=>Re(o,t)?Re(n,o)&&wr(o)===r?s(t):Ai.none("in same cell"):Rs(Li,[o,t]).fold((()=>((e,t,o)=>{const n=e.getRect(t),r=e.getRect(o);return r.right>n.left&&r.lefts(t))))))).getOr(Ai.none("default")),cata:(e,t,o,n,r)=>e.fold(t,o,n,r)},Mi=ue("br"),ji=(e,t,o)=>t(e,o).bind((e=>ie(e)&&0===hr(e).trim().length?ji(e,t,o):C.some(e))),Pi=(e,t,o,n)=>((e,t)=>We(e,t).filter(Mi).orThunk((()=>We(e,t-1).filter(Mi))))(t,o).bind((t=>n.traverse(t).fold((()=>ji(t,n.gather,e).map(n.relative)),(e=>(e=>Ne(e).bind((t=>{const o=Le(t);return((e,t)=>W(e,b(Re,t)))(o,e).map((n=>((e,t,o,n)=>({parent:e,children:t,element:o,index:n}))(t,o,e,n)))})))(e).map((e=>ri.on(e.parent,e.index))))))),Ii=(e,t)=>({left:e.left,top:e.top+t,right:e.right,bottom:e.bottom+t}),Fi=(e,t)=>({left:e.left,top:e.top-t,right:e.right,bottom:e.bottom-t}),Hi=(e,t,o)=>({left:e.left+t,top:e.top+o,right:e.right+t,bottom:e.bottom+o}),$i=e=>({left:e.left,top:e.top,right:e.right,bottom:e.bottom}),Vi=(e,t)=>C.some(e.getRect(t)),qi=(e,t,o)=>ce(t)?Vi(e,t).map($i):ie(t)?((e,t,o)=>o>=0&&o0?e.getRangedRect(t,o-1,t,o):C.none())(e,t,o).map($i):C.none(),Ui=(e,t)=>ce(t)?Vi(e,t).map($i):ie(t)?e.getRangedRect(t,0,t,wr(t)).map($i):C.none(),Gi=sa([{none:[]},{retry:["caret"]}]),Ki=(e,t,o)=>gt(t,xa).fold(y,(t=>Ui(e,t).exists((e=>((e,t)=>e.leftt.right)(o,e))))),Yi={point:e=>e.bottom,adjuster:(e,t,o,n,r)=>{const s=Ii(r,5);return Math.abs(o.bottom-n.bottom)<1||o.top>r.bottom?Gi.retry(s):o.top===r.bottom?Gi.retry(Ii(r,1)):Ki(e,t,r)?Gi.retry(Hi(s,5,0)):Gi.none()},move:Ii,gather:zi},Ji=(e,t,o,n,r)=>0===r?C.some(n):((e,t,o)=>e.elementFromPoint(t,o).filter((e=>"table"===ne(e))).isSome())(e,n.left,t.point(n))?((e,t,o,n,r)=>Ji(e,t,o,t.move(n,5),r))(e,t,o,n,r-1):e.situsFromPoint(n.left,t.point(n)).bind((s=>s.start.fold(C.none,(s=>Ui(e,s).bind((l=>t.adjuster(e,s,l,o,n).fold(C.none,(n=>Ji(e,t,o,n,r-1))))).orThunk((()=>C.some(n)))),C.none))),Qi=(e,t,o)=>{const n=e.move(o,5),r=Ji(t,e,o,n,100).getOr(n);return((e,t,o)=>e.point(t)>o.getInnerHeight()?C.some(e.point(t)-o.getInnerHeight()):e.point(t)<0?C.some(-e.point(t)):C.none())(e,r,t).fold((()=>t.situsFromPoint(r.left,e.point(r))),(o=>(t.scrollBy(0,o),t.situsFromPoint(r.left,e.point(r)-o))))},Xi={tryUp:b(Qi,{point:e=>e.top,adjuster:(e,t,o,n,r)=>{const s=Fi(r,5);return Math.abs(o.top-n.top)<1||o.bottome.getSelection().bind((n=>((e,t,o,n)=>{const r=Mi(t)?((e,t,o)=>o.traverse(t).orThunk((()=>ji(t,o.gather,e))).map(o.relative))(e,t,n):Pi(e,t,o,n);return r.map((e=>({start:e,finish:e})))})(t,n.finish,n.foffset,o).fold((()=>C.some(Gs(n.finish,n.foffset))),(r=>{const s=e.fromSitus(r);return l=Wi.verify(e,n.finish,n.foffset,s.finish,s.foffset,o.failure,t),Wi.cata(l,(e=>C.none()),(()=>C.none()),(e=>C.some(Gs(e,0))),(e=>C.some(Gs(e,wr(e)))));var l})))),em=(e,t,o,n,r,s)=>0===s?C.none():nm(e,t,o,n,r).bind((l=>{const a=e.fromSitus(l),c=Wi.verify(e,o,n,a.finish,a.foffset,r.failure,t);return Wi.cata(c,(()=>C.none()),(()=>C.some(l)),(l=>Re(o,l)&&0===n?tm(e,o,n,Fi,r):em(e,t,l,0,r,s-1)),(l=>Re(o,l)&&n===wr(l)?tm(e,o,n,Ii,r):em(e,t,l,wr(l),r,s-1)))})),tm=(e,t,o,n,r)=>qi(e,t,o).bind((t=>om(e,r,n(t,Xi.getJumpSize())))),om=(e,t,o)=>{const n=No().browser;return n.isChromium()||n.isSafari()||n.isFirefox()?t.retry(e,o):C.none()},nm=(e,t,o,n,r)=>qi(e,o,n).bind((t=>om(e,r,t))),rm=(e,t,o,n,r)=>wt(n,"td,th",t).bind((n=>wt(n,"table",t).bind((s=>((e,t)=>ft(e,(e=>Ne(e).exists((e=>Re(e,t)))),void 0).isSome())(r,s)?((e,t,o)=>Zi(e,t,o).bind((n=>em(e,t,n.element,n.offset,o,20).map(e.fromSitus))))(e,t,o).bind((e=>wt(e.finish,"td,th",t).map((t=>({start:n,finish:t,range:e}))))):C.none())))),sm=(e,t,o,n,r,s)=>s(n,t).orThunk((()=>rm(e,t,o,n,r).map((e=>{const t=e.range;return si(C.some(vi(t.start,t.soffset,t.finish,t.foffset)),!0)})))),lm=(e,t)=>wt(e,"tr",t).bind((e=>wt(e,"table",t).bind((o=>{const n=dt(o,"tr");return Re(e,n[0])?((e,t,o)=>Ei(_i,e,(e=>Cr(e).isSome()),o))(o,0,t).map((e=>{const t=wr(e);return si(C.some(vi(e,t,e,t)),!0)})):C.none()})))),am=(e,t)=>wt(e,"tr",t).bind((e=>wt(e,"table",t).bind((o=>{const n=dt(o,"tr");return Re(e,n[n.length-1])?((e,t,o)=>Ni(_i,e,(e=>xr(e).isSome()),o))(o,0,t).map((e=>si(C.some(vi(e,0,e,0)),!0))):C.none()})))),cm=(e,t,o,n,r,s,l)=>rm(e,o,n,r,s).bind((e=>yi(t,o,e.start,e.finish,l))),im=e=>{let t=e;return{get:()=>t,set:e=>{t=e}}},mm=()=>{const e=(e=>{const t=im(C.none()),o=()=>t.get().each(e);return{clear:()=>{o(),t.set(C.none())},isSet:()=>t.get().isSome(),get:()=>t.get(),set:e=>{o(),t.set(C.some(e))}}})(f);return{...e,on:t=>e.get().each(t)}},dm=(e,t)=>wt(e,"td,th",t),um=e=>_e(e).exists(es),fm={traverse:Ae,gather:zi,relative:ri.before,retry:Xi.tryDown,failure:Wi.failedDown},gm={traverse:ze,gather:Bi,relative:ri.before,retry:Xi.tryUp,failure:Wi.failedUp},hm=e=>t=>t===e,pm=hm(38),bm=hm(40),wm=e=>e>=37&&e<=40,vm={isBackward:hm(37),isForward:hm(39)},ym={isBackward:hm(39),isForward:hm(37)},xm=sa([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),Cm={domRange:xm.domRange,relative:xm.relative,exact:xm.exact,exactFromRange:e=>xm.exact(e.start,e.soffset,e.finish,e.foffset),getWin:e=>{const t=(e=>e.match({domRange:e=>xe.fromDom(e.startContainer),relative:(e,t)=>ri.getStart(e),exact:(e,t,o,n)=>e}))(e);return xe.fromDom(Ee(t).dom.defaultView)},range:pi},Sm=(e,t)=>{const o=ne(e);return"input"===o?ri.after(e):D(["br","img"],o)?0===t?ri.before(e):ri.after(e):ri.on(e,t)},Tm=e=>C.from(e.getSelection()),Rm=(e,t)=>{Tm(e).each((e=>{e.removeAllRanges(),e.addRange(t)}))},Dm=(e,t,o,n,r)=>{const s=mi(e,t,o,n,r);Rm(e,s)},Om=(e,t)=>gi(e,t).match({ltr:(t,o,n,r)=>{Dm(e,t,o,n,r)},rtl:(t,o,n,r)=>{Tm(e).each((s=>{if(s.setBaseAndExtent)s.setBaseAndExtent(t.dom,o,n.dom,r);else if(s.extend)try{((e,t,o,n,r,s)=>{t.collapse(o.dom,n),t.extend(r.dom,s)})(0,s,t,o,n,r)}catch(s){Dm(e,n,r,t,o)}else Dm(e,n,r,t,o)}))}}),km=(e,t,o,n,r)=>{const s=((e,t,o,n)=>{const r=Sm(e,t),s=Sm(o,n);return Cm.relative(r,s)})(t,o,n,r);Om(e,s)},Em=(e,t,o)=>{const n=((e,t)=>{const o=e.fold(ri.before,Sm,ri.after),n=t.fold(ri.before,Sm,ri.after);return Cm.relative(o,n)})(t,o);Om(e,n)},Nm=e=>{if(e.rangeCount>0){const t=e.getRangeAt(0),o=e.getRangeAt(e.rangeCount-1);return C.some(pi(xe.fromDom(t.startContainer),t.startOffset,xe.fromDom(o.endContainer),o.endOffset))}return C.none()},_m=e=>{if(null===e.anchorNode||null===e.focusNode)return Nm(e);{const t=xe.fromDom(e.anchorNode),o=xe.fromDom(e.focusNode);return((e,t,o,n)=>{const r=((e,t,o,n)=>{const r=ke(e).dom.createRange();return r.setStart(e.dom,t),r.setEnd(o.dom,n),r})(e,t,o,n),s=Re(e,o)&&t===n;return r.collapsed&&!s})(t,e.anchorOffset,o,e.focusOffset)?C.some(pi(t,e.anchorOffset,o,e.focusOffset)):Nm(e)}},Bm=(e,t,o=!0)=>{const n=(o?ai:li)(e,t);Rm(e,n)},zm=e=>(e=>Tm(e).filter((e=>e.rangeCount>0)).bind(_m))(e).map((e=>Cm.exact(e.start,e.soffset,e.finish,e.foffset))),Am=(e,t,o)=>((e,t,o)=>((e,t,o)=>e.caretPositionFromPoint?((e,t,o)=>{var n;return C.from(null===(n=e.caretPositionFromPoint)||void 0===n?void 0:n.call(e,t,o)).bind((t=>{if(null===t.offsetNode)return C.none();const o=e.createRange();return o.setStart(t.offsetNode,t.offset),o.collapse(),C.some(o)}))})(e,t,o):e.caretRangeFromPoint?((e,t,o)=>{var n;return C.from(null===(n=e.caretRangeFromPoint)||void 0===n?void 0:n.call(e,t,o))})(e,t,o):C.none())(e.document,t,o).map((e=>pi(xe.fromDom(e.startContainer),e.startOffset,xe.fromDom(e.endContainer),e.endOffset))))(e,t,o),Lm=e=>({elementFromPoint:(t,o)=>xe.fromPoint(xe.fromDom(e.document),t,o),getRect:e=>e.dom.getBoundingClientRect(),getRangedRect:(t,o,n,r)=>{const s=Cm.exact(t,o,n,r);return((e,t)=>(e=>{const t=e.getClientRects(),o=t.length>0?t[0]:e.getBoundingClientRect();return o.width>0||o.height>0?C.some(o).map(di):C.none()})(hi(e,t)))(e,s)},getSelection:()=>zm(e).map((t=>wi(e,t))),fromSitus:t=>{const o=Cm.relative(t.start,t.finish);return wi(e,o)},situsFromPoint:(t,o)=>Am(e,t,o).map((e=>bi(e.start,e.soffset,e.finish,e.foffset))),clearSelection:()=>{(e=>{Tm(e).each((e=>e.removeAllRanges()))})(e)},collapseSelection:(t=!1)=>{zm(e).each((o=>o.fold((e=>e.collapse(t)),((o,n)=>{const r=t?o:n;Em(e,r,r)}),((o,n,r,s)=>{const l=t?o:r,a=t?n:s;km(e,l,a,l,a)}))))},setSelection:t=>{km(e,t.start,t.soffset,t.finish,t.foffset)},setRelativeSelection:(t,o)=>{Em(e,t,o)},selectNode:t=>{Bm(e,t,!1)},selectContents:t=>{Bm(e,t)},getInnerHeight:()=>e.innerHeight,getScrollY:()=>(e=>{const t=void 0!==e?e.dom:document,o=t.body.scrollLeft||t.documentElement.scrollLeft,n=t.body.scrollTop||t.documentElement.scrollTop;return bn(o,n)})(xe.fromDom(e.document)).top,scrollBy:(t,o)=>{((e,t,o)=>{const n=(void 0!==o?o.dom:document).defaultView;n&&n.scrollBy(e,t)})(t,o,xe.fromDom(e.document))}}),Wm=(e,t)=>({rows:e,cols:t}),Mm=e=>gt(e,ae).exists(es),jm=(e,t)=>Mm(e)||Mm(t),Pm=e=>void 0!==e.dom.classList,Im=(e,t)=>((e,t,o)=>{const n=((e,t)=>{const o=pe(e,t);return void 0===o||""===o?[]:o.split(" ")})(e,t).concat([o]);return ge(e,t,n.join(" ")),!0})(e,"class",t),Fm=(e,t)=>{Pm(e)?e.dom.classList.add(t):Im(e,t)},Hm=(e,t)=>Pm(e)&&e.dom.classList.contains(t),$m=()=>({tag:"none"}),Vm=e=>({tag:"multiple",elements:e}),qm=e=>({tag:"single",element:e}),Um=e=>{const t=xe.fromDom((e=>{if(m(e.target)){const t=xe.fromDom(e.target);if(ce(t)&&m(t.dom.shadowRoot)&&e.composed&&e.composedPath){const t=e.composedPath();if(t)return H(t)}}return C.from(e.target)})(e).getOr(e.target)),o=()=>e.stopPropagation(),n=()=>e.preventDefault(),r=(s=n,l=o,(...e)=>s(l.apply(null,e)));var s,l;return((e,t,o,n,r,s,l)=>({target:e,x:t,y:o,stop:n,prevent:r,kill:s,raw:l}))(t,e.clientX,e.clientY,o,n,r,e)},Gm=(e,t,o,n)=>{e.dom.removeEventListener(t,o,n)},Km=x,Ym=(e,t,o)=>((e,t,o,n)=>((e,t,o,n,r)=>{const s=((e,t)=>o=>{e(o)&&t(Um(o))})(o,n);return e.dom.addEventListener(t,s,r),{unbind:b(Gm,e,t,s,r)}})(e,t,o,n,!1))(e,t,Km,o),Jm=Um,Qm=e=>!Hm(xe.fromDom(e.target),"ephox-snooker-resizer-bar"),Xm=(e,t)=>{const o=(r=Is.selectedSelector,{get:()=>_s(xe.fromDom(e.getBody()),r).fold((()=>Vs(ss(e),ns(e)).fold($m,qm)),Vm)}),n=((e,t,o)=>{const n=t=>{we(t,e.selected),we(t,e.firstSelected),we(t,e.lastSelected)},r=t=>{ge(t,e.selected,"1")},s=e=>{l(e),o()},l=t=>{const o=dt(t,`${e.selectedSelector},${e.firstSelectedSelector},${e.lastSelectedSelector}`);N(o,n)};return{clearBeforeUpdate:l,clear:s,selectRange:(o,n,l,a)=>{s(o),N(n,r),ge(l,e.firstSelected,"1"),ge(a,e.lastSelected,"1"),t(n,l,a)},selectedSelector:e.selectedSelector,firstSelectedSelector:e.firstSelectedSelector,lastSelectedSelector:e.lastSelectedSelector}})(Is,((t,o,n)=>{Gt(o).each((r=>{const s=E(t,(e=>e.dom)),l=Wr(e),a=_r(f,xe.fromDom(e.getDoc()),l),c=((e,t,o)=>{const n=Xo(e);return zl(n,t).map((e=>{const t=Ol(n,o,!1),{rows:r}=Vo(t),s=((e,t)=>{const o=e.slice(0,t[t.length-1].row+1),n=kl(o);return j(n,(e=>{const o=e.cells.slice(0,t[t.length-1].column+1);return E(o,(e=>e.element))}))})(r,e),l=((e,t)=>{const o=e.slice(t[0].row+t[0].rowspan-1,e.length),n=kl(o);return j(n,(e=>{const o=e.cells.slice(t[0].column+t[0].colspan-1,e.cells.length);return E(o,(e=>e.element))}))})(r,e);return{upOrLeftCells:s,downOrRightCells:l}}))})(r,{selection:qs(e)},a).map((e=>K(e,(e=>E(e,(e=>e.dom)))))).getOrUndefined();((e,t,o,n,r)=>{e.dispatch("TableSelectionChange",{cells:t,start:o,finish:n,otherCells:r})})(e,s,o.dom,n.dom,c)}))}),(()=>(e=>{e.dispatch("TableSelectionClear")})(e)));var r;return e.on("init",(o=>{const r=e.getWin(),s=os(e),l=ns(e),a=((e,t,o,n)=>{const r=((e,t,o,n)=>{const r=mm(),s=r.clear,l=s=>{r.on((r=>{n.clearBeforeUpdate(t),dm(s.target,o).each((l=>{Os(r,l,o).each((o=>{const r=o.boxes.getOr([]);if(1===r.length){const e=r[0],o="false"===ts(e),l=vt(Zr(s.target),e,Re);o&&l&&n.selectRange(t,r,e,e)}else r.length>1&&(n.selectRange(t,r,o.start,o.finish),e.selectContents(l))}))}))}))};return{clearstate:s,mousedown:e=>{n.clear(t),dm(e.target,o).filter(um).each(r.set)},mouseover:e=>{l(e)},mouseup:e=>{l(e),s()}}})(Lm(e),t,o,n);return{clearstate:r.clearstate,mousedown:r.mousedown,mouseover:r.mouseover,mouseup:r.mouseup}})(r,s,l,n),c=((e,t,o,n)=>{const r=Lm(e),s=()=>(n.clear(t),C.none());return{keydown:(e,l,a,c,i,m)=>{const d=e.raw,u=d.which,f=!0===d.shiftKey,g=ks(t,n.selectedSelector).fold((()=>(wm(u)&&!f&&n.clearBeforeUpdate(t),wm(u)&&f&&!jm(l,c)?C.none:bm(u)&&f?b(cm,r,t,o,fm,c,l,n.selectRange):pm(u)&&f?b(cm,r,t,o,gm,c,l,n.selectRange):bm(u)?b(sm,r,o,fm,c,l,am):pm(u)?b(sm,r,o,gm,c,l,lm):C.none)),(e=>{const o=o=>()=>{const s=V(o,(o=>((e,t,o,n,r)=>Ns(n,e,t,r.firstSelectedSelector,r.lastSelectedSelector).map((e=>(r.clearBeforeUpdate(o),r.selectRange(o,e.boxes,e.start,e.finish),e.boxes))))(o.rows,o.cols,t,e,n)));return s.fold((()=>Es(t,n.firstSelectedSelector,n.lastSelectedSelector).map((e=>{const o=bm(u)||m.isForward(u)?ri.after:ri.before;return r.setRelativeSelection(ri.on(e.first,0),o(e.table)),n.clear(t),si(C.none(),!0)}))),(e=>C.some(si(C.none(),!0))))};return wm(u)&&f&&!jm(l,c)?C.none:bm(u)&&f?o([Wm(1,0)]):pm(u)&&f?o([Wm(-1,0)]):m.isBackward(u)&&f?o([Wm(0,-1),Wm(-1,0)]):m.isForward(u)&&f?o([Wm(0,1),Wm(1,0)]):wm(u)&&!f?s:C.none}));return g()},keyup:(e,r,s,l,a)=>ks(t,n.selectedSelector).fold((()=>{const c=e.raw,i=c.which;return!0===c.shiftKey&&wm(i)&&jm(r,l)?((e,t,o,n,r,s,l)=>Re(o,r)&&n===s?C.none():wt(o,"td,th",t).bind((o=>wt(r,"td,th",t).bind((n=>yi(e,t,o,n,l))))))(t,o,r,s,l,a,n.selectRange):C.none()}),C.none)}})(r,s,l,n),i=((e,t,o,n)=>{const r=Lm(e);return(e,s)=>{n.clearBeforeUpdate(t),Os(e,s,o).each((e=>{const o=e.boxes.getOr([]);n.selectRange(t,o,e.start,e.finish),r.selectContents(s),r.collapseSelection()}))}})(r,s,l,n);e.on("TableSelectorChange",(e=>i(e.start,e.finish)));const m=(t,o)=>{(e=>!0===e.raw.shiftKey)(t)&&(o.kill&&t.kill(),o.selection.each((t=>{const o=Cm.relative(t.start,t.finish),n=hi(r,o);e.selection.setRng(n)})))},d=e=>0===e.button,u=(()=>{const e=im(xe.fromDom(s)),t=im(0);return{touchEnd:o=>{const n=xe.fromDom(o.target);if(ue("td")(n)||ue("th")(n)){const r=e.get(),s=t.get();Re(r,n)&&o.timeStamp-s<300&&(o.preventDefault(),i(n,n))}e.set(n),t.set(o.timeStamp)}}})();e.on("dragstart",(e=>{a.clearstate()})),e.on("mousedown",(e=>{d(e)&&Qm(e)&&a.mousedown(Jm(e))})),e.on("mouseover",(e=>{var t;(void 0===(t=e).buttons||1&t.buttons)&&Qm(e)&&a.mouseover(Jm(e))})),e.on("mouseup",(e=>{d(e)&&Qm(e)&&a.mouseup(Jm(e))})),e.on("touchend",u.touchEnd),e.on("keyup",(t=>{const o=Jm(t);if(o.raw.shiftKey&&wm(o.raw.which)){const t=e.selection.getRng(),n=xe.fromDom(t.startContainer),r=xe.fromDom(t.endContainer);c.keyup(o,n,t.startOffset,r,t.endOffset).each((e=>{m(o,e)}))}})),e.on("keydown",(o=>{const n=Jm(o);t.hide();const r=e.selection.getRng(),s=xe.fromDom(r.startContainer),l=xe.fromDom(r.endContainer),a=dn(vm,ym)(xe.fromDom(e.selection.getStart()));c.keydown(n,s,r.startOffset,l,r.endOffset,a).each((e=>{m(n,e)})),t.show()})),e.on("NodeChange",(()=>{const t=e.selection,o=xe.fromDom(t.getStart()),r=xe.fromDom(t.getEnd());Rs(Gt,[o,r]).fold((()=>n.clear(s)),f)}))})),e.on("PreInit",(()=>{e.serializer.addTempAttr(Is.firstSelected),e.serializer.addTempAttr(Is.lastSelected)})),{getSelectedCells:()=>((e,t)=>{switch(e.tag){case"none":return t();case"single":return(e=>[e.dom])(e.element);case"multiple":return(e=>E(e,(e=>e.dom)))(e.elements)}})(o.get(),g([])),clearSelectedCells:e=>n.clear(xe.fromDom(e))}},Zm=e=>{let t=[];return{bind:e=>{if(void 0===e)throw new Error("Event bind error: undefined handler");t.push(e)},unbind:e=>{t=B(t,(t=>t!==e))},trigger:(...o)=>{const n={};N(e,((e,t)=>{n[e]=o[t]})),N(t,(e=>{e(n)}))}}},ed=e=>({registry:K(e,(e=>({bind:e.bind,unbind:e.unbind}))),trigger:K(e,(e=>e.trigger))}),td=e=>e.slice(0).sort(),od=(e,t)=>{const o=B(t,(t=>!D(e,t)));o.length>0&&(e=>{throw new Error("Unsupported keys for object: "+td(e).join(", "))})(o)},nd=e=>((e,t)=>((e,t,o)=>{if(0===t.length)throw new Error("You must specify at least one required field.");return((e,t)=>{if(!l(t))throw new Error("The "+e+" fields must be an array. Was: "+t+".");N(t,(t=>{if(!r(t))throw new Error("The value "+t+" in the "+e+" fields was not a string.")}))})("required",t),(e=>{const t=td(e);L(t,((e,o)=>o{throw new Error("The field: "+e+" occurs more than once in the combined fields: ["+t.join(", ")+"].")}))})(t),n=>{const r=q(n);P(t,(e=>D(r,e)))||((e,t)=>{throw new Error("All required keys ("+td(e).join(", ")+") were not specified. Specified keys were: "+td(t).join(", ")+".")})(t,r),e(t,r);const s=B(t,(e=>!o.validate(n[e],e)));return s.length>0&&((e,t)=>{throw new Error("All values need to be of type: "+t+". Keys ("+td(e).join(", ")+") were not.")})(s,o.label),n}})(e,t,{validate:d,label:"function"}))(od,e),rd=nd(["compare","extract","mutate","sink"]),sd=nd(["element","start","stop","destroy"]),ld=nd(["forceDrop","drop","move","delayDrop"]),ad=()=>{const e=(()=>{const e=ed({move:Zm(["info"])});return{onEvent:f,reset:f,events:e.registry}})(),t=(()=>{let e=C.none();const t=ed({move:Zm(["info"])});return{onEvent:(o,n)=>{n.extract(o).each((o=>{const r=((t,o)=>{const n=e.map((e=>t.compare(e,o)));return e=C.some(o),n})(n,o);r.each((e=>{t.trigger.move(e)}))}))},reset:()=>{e=C.none()},events:t.registry}})();let o=e;return{on:()=>{o.reset(),o=t},off:()=>{o.reset(),o=e},isOn:()=>o===t,onEvent:(e,t)=>{o.onEvent(e,t)},events:t.events}},cd=e=>{const t=e.replace(/\./g,"-");return{resolve:e=>t+"-"+e}},id=cd("ephox-dragster").resolve;var md=rd({compare:(e,t)=>bn(t.left-e.left,t.top-e.top),extract:e=>C.some(bn(e.x,e.y)),sink:(e,t)=>{const o=(e=>{const t={layerClass:id("blocker"),...e},o=xe.fromTag("div");return ge(o,"role","presentation"),_t(o,{position:"fixed",left:"0px",top:"0px",width:"100%",height:"100%"}),Fm(o,id("blocker")),Fm(o,t.layerClass),{element:g(o),destroy:()=>{qe(o)}}})(t),n=Ym(o.element(),"mousedown",e.forceDrop),r=Ym(o.element(),"mouseup",e.drop),s=Ym(o.element(),"mousemove",e.move),l=Ym(o.element(),"mouseout",e.delayDrop);return sd({element:o.element,start:e=>{Ie(e,o.element())},stop:()=>{qe(o.element())},destroy:()=>{o.destroy(),r.unbind(),s.unbind(),l.unbind(),n.unbind()}})},mutate:(e,t)=>{e.mutate(t.left,t.top)}});const dd=cd("ephox-snooker").resolve,ud=dd("resizer-bar"),fd=dd("resizer-rows"),gd=dd("resizer-cols"),hd=e=>{const t=dt(e.parent(),"."+ud);N(t,qe)},pd=(e,t,o)=>{const n=e.origin();N(t,(t=>{t.each((t=>{const r=o(n,t);Fm(r,ud),Ie(e.parent(),r)}))}))},bd=(e,t,o,n,r)=>{const s=vn(o),l=t.isResizable,a=n.length>0?_n.positions(n,o):[],c=a.length>0?((e,t)=>j(e.all,((e,o)=>t(e.element)?[o]:[])))(e,l):[];((e,t,o,n)=>{pd(e,t,((e,t)=>{const r=((e,t,o,n)=>{const r=xe.fromTag("div");return _t(r,{position:"absolute",left:t+"px",top:o-3.5+"px",height:"7px",width:n+"px"}),he(r,{"data-row":e,role:"presentation"}),r})(t.row,o.left-e.left,t.y-e.top,n);return Fm(r,fd),r}))})(t,B(a,((e,t)=>O(c,(e=>t===e)))),s,Lo(o));const i=r.length>0?zn.positions(r,o):[],m=i.length>0?((e,t)=>{const o=[];return k(e.grid.columns,(n=>{ln(e,n).map((e=>e.element)).forall(t)&&o.push(n)})),B(o,(o=>{const n=on(e,(e=>e.column===o));return P(n,(e=>t(e.element)))}))})(e,l):[];((e,t,o,n)=>{pd(e,t,((e,t)=>{const r=((e,t,o,n,r)=>{const s=xe.fromTag("div");return _t(s,{position:"absolute",left:t-3.5+"px",top:o+"px",height:r+"px",width:"7px"}),he(s,{"data-column":e,role:"presentation"}),s})(t.col,t.x-e.left,o.top-e.top,0,n);return Fm(r,gd),r}))})(t,B(i,((e,t)=>O(m,(e=>t===e)))),s,hn(o))},wd=(e,t)=>{if(hd(e),e.isResizable(t)){const o=Xo(t),n=mn(o),r=an(o);bd(o,e,t,n,r)}},vd=(e,t)=>{const o=dt(e.parent(),"."+ud);N(o,t)},yd=e=>{vd(e,(e=>{Nt(e,"display","none")}))},xd=e=>{vd(e,(e=>{Nt(e,"display","block")}))},Cd=dd("resizer-bar-dragging"),Sd=e=>{const t=(()=>{const e=ed({drag:Zm(["xDelta","yDelta","target"])});let t=C.none();const o=(()=>{const e=ed({drag:Zm(["xDelta","yDelta"])});return{mutate:(t,o)=>{e.trigger.drag(t,o)},events:e.registry}})();return o.events.drag.bind((o=>{t.each((t=>{e.trigger.drag(o.xDelta,o.yDelta,t)}))})),{assign:e=>{t=C.some(e)},get:()=>t,mutate:o.mutate,events:e.registry}})(),o=((e,t={})=>{var o;return((e,t,o)=>{let n=!1;const r=ed({start:Zm([]),stop:Zm([])}),s=ad(),l=()=>{m.stop(),s.isOn()&&(s.off(),r.trigger.stop())},c=(e=>{let t=null;const o=()=>{a(t)||(clearTimeout(t),t=null)};return{cancel:o,throttle:(...n)=>{o(),t=setTimeout((()=>{t=null,e.apply(null,n)}),200)}}})(l);s.events.move.bind((o=>{t.mutate(e,o.info)}));const i=e=>(...t)=>{n&&e.apply(null,t)},m=t.sink(ld({forceDrop:l,drop:i(l),move:i((e=>{c.cancel(),s.onEvent(e,t)})),delayDrop:i(c.throttle)}),o);return{element:m.element,go:e=>{m.start(e),s.on(),r.trigger.start()},on:()=>{n=!0},off:()=>{n=!1},isActive:()=>n,destroy:()=>{m.destroy()},events:r.registry}})(e,null!==(o=t.mode)&&void 0!==o?o:md,t)})(t,{});let n=C.none();const r=(e,t)=>C.from(pe(e,t));t.events.drag.bind((e=>{r(e.target,"data-row").each((t=>{const o=Pt(e.target,"top");Nt(e.target,"top",o+e.yDelta+"px")})),r(e.target,"data-column").each((t=>{const o=Pt(e.target,"left");Nt(e.target,"left",o+e.xDelta+"px")}))}));const s=(e,t)=>Pt(e,t)-Wt(e,"data-initial-"+t,0);o.events.stop.bind((()=>{t.get().each((t=>{n.each((o=>{r(t,"data-row").each((e=>{const n=s(t,"top");we(t,"data-initial-top"),d.trigger.adjustHeight(o,n,parseInt(e,10))})),r(t,"data-column").each((e=>{const n=s(t,"left");we(t,"data-initial-left"),d.trigger.adjustWidth(o,n,parseInt(e,10))})),wd(e,o)}))}))}));const l=(n,r)=>{d.trigger.startAdjust(),t.assign(n),ge(n,"data-initial-"+r,Pt(n,r)),Fm(n,Cd),Nt(n,"opacity","0.2"),o.go(e.dragContainer())},c=Ym(e.parent(),"mousedown",(e=>{var t;t=e.target,Hm(t,fd)&&l(e.target,"top"),(e=>Hm(e,gd))(e.target)&&l(e.target,"left")})),i=t=>Re(t,e.view()),m=Ym(e.view(),"mouseover",(t=>{var r;(r=t.target,wt(r,"table",i).filter(es)).fold((()=>{st(t.target)&&hd(e)}),(t=>{o.isActive()&&(n=C.some(t),wd(e,t))}))})),d=ed({adjustHeight:Zm(["table","delta","row"]),adjustWidth:Zm(["table","delta","column"]),startAdjust:Zm([])});return{destroy:()=>{c.unbind(),m.unbind(),o.destroy(),hd(e)},refresh:t=>{wd(e,t)},on:o.on,off:o.off,hideBars:b(yd,e),showBars:b(xd,e),events:d.registry}};let Td=0;const Rd=(e,t)=>{const o=(e=>!(e=>e.inline&&(e=>{var t;if(!e.inline)return C.none();const o=null!==(t=Jr(e))&&void 0!==t?t:"";if(o.length>0)return bt(lt(),o);const n=Qr(e);return m(n)?C.some(xe.fromDom(n)):C.none()})(e).isSome())(e)&&"split"===Xr(e))(e),n=xe.fromDom(e.getBody()),r=(e=>{const t=(e=>{const t=(new Date).getTime(),o=Math.floor(window.crypto.getRandomValues(new Uint32Array(1))[0]/4294967295*1e9);return Td++,e+"_"+o+Td+String(t)})("resizer-container"),o=xe.fromTag("div");return ge(o,"id",t),_t(o,{position:e,height:"0",width:"0",padding:"0",margin:"0",border:"0"}),o})(o?"relative":"static"),s=lt();return o?(je(n,r),((e,t,o,n)=>({parent:g(t),view:g(e),dragContainer:g(o),origin:()=>vn(t),isResizable:n}))(n,r,s,t)):(Ie(s,r),((e,t,o)=>({parent:g(t),view:g(e),dragContainer:g(t),origin:g(bn(0,0)),isResizable:o}))(n,r,t))},Dd=e=>m(e)&&"TABLE"===e.nodeName,Od="bar-",kd=e=>"false"!==pe(e,"data-mce-resize"),Ed=e=>{const t=mm(),o=mm(),n=mm();let r,s,l,a;const c=t=>wc(e,t),i=()=>Pr(e)?ll():sl(),m=(t,o,n,m)=>{const d=(e=>{return Tt(t=e,"corner-")?(e=>e.substring(7))(t):t;var t})(o),u=Rt(d,"e"),f=Tt(d,"n");if(""===s&&Lc(t),""===a&&(e=>{const t=(e=>gn(e)+"px")(e);Nc(e,C.none(),C.some(t)),Ac(e)})(t),n!==r&&""!==s){Nt(t,"width",s);const o=i(),l=c(t),a=Pr(e)||u?(e=>al(e).columns)(t)-1:0;ca(t,n-r,a,o,l)}else if((e=>/^(\d+(\.\d+)?)%$/.test(e))(s)){const e=parseFloat(s.replace("%",""));Nt(t,"width",n*e/r+"%")}if((e=>/^(\d+(\.\d+)?)px$/.test(e))(s)&&(e=>{const t=Xo(e);sn(t)||N(qt(e),(e=>{const t=Bt(e,"width");Nt(e,"width",t),we(e,"width")}))})(t),m!==l&&""!==a){Nt(t,"height",a);const e=f?0:(e=>al(e).rows)(t)-1;ia(t,m-l,e)}};e.on("init",(()=>{const r=((e,t)=>e.inline?Rd(e,t):((e,t)=>{const o=me(e)?(e=>xe.fromDom(Ee(e).dom.documentElement))(e):e;return{parent:g(o),view:g(e),dragContainer:g(o),origin:g(bn(0,0)),isResizable:t}})(xe.fromDom(e.getDoc()),t))(e,kd);if(n.set(r),(e=>{const t=e.options.get("object_resizing");return D(t.split(","),"table")})(e)&&qr(e)){const n=((e,t,o)=>{const n=_n,r=zn,s=Sd(e),l=ed({beforeResize:Zm(["table","type"]),afterResize:Zm(["table","type"]),startDrag:Zm([])});return s.events.adjustHeight.bind((e=>{const t=e.table;l.trigger.beforeResize(t,"row");const o=n.delta(e.delta,t);ia(t,o,e.row),l.trigger.afterResize(t,"row")})),s.events.startAdjust.bind((e=>{l.trigger.startDrag()})),s.events.adjustWidth.bind((e=>{const n=e.table;l.trigger.beforeResize(n,"col");const s=r.delta(e.delta,n),a=o(n);ca(n,s,e.column,t,a),l.trigger.afterResize(n,"col")})),{on:s.on,off:s.off,refreshBars:s.refresh,hideBars:s.hideBars,showBars:s.showBars,destroy:s.destroy,events:l.registry}})(r,i(),c);e.mode.isReadOnly()||n.on(),n.events.startDrag.bind((o=>{t.set(e.selection.getRng())})),n.events.beforeResize.bind((t=>{const o=t.table.dom;((e,t,o,n,r)=>{e.dispatch("ObjectResizeStart",{target:t,width:o,height:n,origin:r})})(e,o,ls(o),as(o),Od+t.type)})),n.events.afterResize.bind((o=>{const n=o.table,r=n.dom;rs(n),t.on((t=>{e.selection.setRng(t),e.focus()})),((e,t,o,n,r)=>{e.dispatch("ObjectResized",{target:t,width:o,height:n,origin:r})})(e,r,ls(r),as(r),Od+o.type),e.undoManager.add()})),o.set(n)}})),e.on("ObjectResizeStart",(t=>{const o=t.target;if(Dd(o)&&!e.mode.isReadOnly()){const n=xe.fromDom(o);N(e.dom.select(".mce-clonedresizable"),(t=>{e.dom.addClass(t,"mce-"+jr(e)+"-columns")})),!Bc(n)&&$r(e)?Wc(n):!_c(n)&&Hr(e)&&Lc(n),zc(n)&&Tt(t.origin,Od)&&Lc(n),r=t.width,s=Vr(e)?"":is(e,o).getOr(""),l=t.height,a=ms(e,o).getOr("")}})),e.on("ObjectResized",(t=>{const o=t.target;if(Dd(o)){const n=xe.fromDom(o),r=t.origin;(e=>Tt(e,"corner-"))(r)&&m(n,r,t.width,t.height),rs(n),gc(e,n.dom,hc)}}));const d=()=>{o.on((e=>{e.on(),e.showBars()}))},u=()=>{o.on((e=>{e.off(),e.hideBars()}))};return e.on("DisabledStateChange",(e=>{e.state?u():d()})),e.on("SwitchMode",(()=>{e.mode.isReadOnly()?u():d()})),e.on("dragstart dragend",(e=>{"dragstart"===e.type?u():d()})),e.on("remove",(()=>{o.on((e=>{e.destroy()})),n.on((t=>{((e,t)=>{e.inline&&qe(t.parent())})(e,t)}))})),{refresh:e=>{o.on((t=>t.refreshBars(xe.fromDom(e))))},hide:()=>{o.on((e=>e.hideBars()))},show:()=>{o.on((e=>e.showBars()))}}},Nd=e=>{(e=>{const t=e.options.register;t("table_clone_elements",{processor:"string[]"}),t("table_use_colgroups",{processor:"boolean",default:!0}),t("table_header_type",{processor:e=>{const t=D(["section","cells","sectionCells","auto"],e);return t?{value:e,valid:t}:{valid:!1,message:"Must be one of: section, cells, sectionCells or auto."}},default:"section"}),t("table_sizing_mode",{processor:"string",default:"auto"}),t("table_default_attributes",{processor:"object",default:{border:"1"}}),t("table_default_styles",{processor:"object",default:{"border-collapse":"collapse"}}),t("table_column_resizing",{processor:e=>{const t=D(["preservetable","resizetable"],e);return t?{value:e,valid:t}:{valid:!1,message:"Must be preservetable, or resizetable."}},default:"preservetable"}),t("table_resize_bars",{processor:"boolean",default:!0}),t("table_style_by_css",{processor:"boolean",default:!0}),t("table_merge_content_on_paste",{processor:"boolean",default:!0})})(e);const t=Ed(e),o=Xm(e,t),n=vc(e,t,o);return oi(e,n),((e,t)=>{const o=ns(e),n=t=>Vs(ss(e)).bind((n=>Gt(n,o).map((o=>{const r=Fs(qs(e),o,n);return t(o,r)})))).getOr("");G({mceTableRowType:()=>n(t.getTableRowType),mceTableCellType:()=>n(t.getTableCellType),mceTableColType:()=>n(t.getTableColType)},((t,o)=>e.addQueryValueHandler(o,t)))})(e,n),Us(e,n),{getSelectedCells:o.getSelectedCells,clearSelectedCells:o.clearSelectedCells}};e.add("dom",(e=>({table:Nd(e)})))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/accordion/plugin.min.js b/src/main/webapp/content/tinymce/plugins/accordion/plugin.min.js new file mode 100644 index 0000000..3c88604 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/accordion/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");let t=0;const o=e=>t=>typeof t===e,n=e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(o=n=e,(r=String).prototype.isPrototypeOf(o)||(null===(s=n.constructor)||void 0===s?void 0:s.name)===r.name)?"string":t;var o,n,r,s})(e),r=o("boolean"),s=e=>null==e,i=e=>!s(e),a=o("function"),d=o("number"),l=e=>()=>e,c=(e,t)=>e===t,m=l(!1);class u{constructor(e,t){this.tag=e,this.value=t}static some(e){return new u(!0,e)}static none(){return u.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?u.some(e(this.value)):u.none()}bind(e){return this.tag?e(this.value):u.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:u.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return i(e)?u.some(e):u.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}u.singletonNone=new u(!1);const g=Array.prototype.indexOf,p=(e,t)=>{return o=e,n=t,g.call(o,n)>-1;var o,n},h=(e,t)=>{const o=e.length,n=new Array(o);for(let r=0;r{for(let o=0,n=e.length;oe.dom.nodeName.toLowerCase(),w=e=>e.dom.nodeType,b=e=>t=>w(t)===e,N=b(1),T=b(3),A=b(9),C=b(11),S=(e,t,o)=>{if(!(n(o)||r(o)||d(o)))throw console.error("Invalid call to Attribute.set. Key ",t,":: Value ",o,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,o+"")},x=(e,t)=>{const o=e.dom.getAttribute(t);return null===o?void 0:o},D=(e,t)=>u.from(x(e,t)),E=(e,t)=>{e.dom.removeAttribute(t)},O=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},M={fromHtml:(e,t)=>{const o=(t||document).createElement("div");if(o.innerHTML=e,!o.hasChildNodes()||o.childNodes.length>1){const t="HTML does not have a single root node";throw console.error(t,e),new Error(t)}return O(o.childNodes[0])},fromTag:(e,t)=>{const o=(t||document).createElement(e);return O(o)},fromText:(e,t)=>{const o=(t||document).createTextNode(e);return O(o)},fromDom:O,fromPoint:(e,t,o)=>u.from(e.dom.elementFromPoint(t,o)).map(O)},P=(e,t)=>{const o=e.dom;if(1!==o.nodeType)return!1;{const e=o;if(void 0!==e.matches)return e.matches(t);if(void 0!==e.msMatchesSelector)return e.msMatchesSelector(t);if(void 0!==e.webkitMatchesSelector)return e.webkitMatchesSelector(t);if(void 0!==e.mozMatchesSelector)return e.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")}},R=e=>1!==e.nodeType&&9!==e.nodeType&&11!==e.nodeType||0===e.childElementCount,k=P,B=(L=/^\s+|\s+$/g,e=>e.replace(L,""));var L;const $=e=>void 0!==e.style&&a(e.style.getPropertyValue),V=e=>u.from(e.dom.parentNode).map(M.fromDom),I=e=>u.from(e.dom.nextSibling).map(M.fromDom),j=e=>h(e.dom.childNodes,M.fromDom),q=e=>M.fromDom(e.dom.host),F=e=>{const t=T(e)?e.dom.parentNode:e.dom;if(null==t||null===t.ownerDocument)return!1;const o=t.ownerDocument;return(e=>{const t=(e=>M.fromDom(e.dom.getRootNode()))(e);return C(o=t)&&i(o.dom.host)?u.some(t):u.none();var o})(M.fromDom(t)).fold((()=>o.body.contains(t)),(n=F,r=q,e=>n(r(e))));var n,r},H=(e,t)=>$(e)?e.style.getPropertyValue(t):"",z=(e,t)=>{V(e).each((o=>{o.dom.insertBefore(t.dom,e.dom)}))},K=(e,t)=>{I(e).fold((()=>{V(e).each((e=>{U(e,t)}))}),(e=>{z(e,t)}))},U=(e,t)=>{e.dom.appendChild(t.dom)},Y=(e,t)=>{f(t,((o,n)=>{const r=0===n?e:t[n-1];K(r,o)}))},_=(e,t)=>{let o=[];return f(j(e),(e=>{t(e)&&(o=o.concat([e])),o=o.concat(_(e,t))})),o},G=(e,t,o)=>{let n=e.dom;const r=a(o)?o:m;for(;n.parentNode;){n=n.parentNode;const e=M.fromDom(n);if(t(e))return u.some(e);if(r(e))break}return u.none()},J=e=>{const t=e.dom;null!==t.parentNode&&t.parentNode.removeChild(t)},Q=(e,t,o)=>G(e,(e=>P(e,t)),o),W=(e,t)=>((e,t)=>{const o=void 0===t?document:t.dom;return R(o)?u.none():u.from(o.querySelector(e)).map(M.fromDom)})(t,e),X=(e=>{const t=t=>e(t)?u.from(t.dom.nodeValue):u.none();return{get:o=>{if(!e(o))throw new Error("Can only get text value of a text node");return t(o).getOr("")},getOption:t,set:(t,o)=>{if(!e(t))throw new Error("Can only set raw text value of a text node");t.dom.nodeValue=o}}})(T);var Z=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","li","table","thead","tbody","tfoot","caption","tr","td","th","h1","h2","h3","h4","h5","h6","blockquote","pre","address"];const ee=(e,t)=>({element:e,offset:t}),te=(e,t,o)=>e.property().isText(t)&&0===e.property().getText(t).trim().length||e.property().isComment(t)?o(t).bind((t=>te(e,t,o).orThunk((()=>u.some(t))))):u.none(),oe=(e,t)=>e.property().isText(t)?e.property().getText(t).length:e.property().children(t).length,ne=(e,t)=>{const o=te(e,t,e.query().prevSibling).getOr(t);if(e.property().isText(o))return ee(o,oe(e,o));const n=e.property().children(o);return n.length>0?ne(e,n[n.length-1]):ee(o,oe(e,o))},re=ne,se={up:l({selector:Q,closest:(e,t,o)=>((e,t,o,n,r)=>((e,t)=>P(e,t))(o,n)?u.some(o):a(r)&&r(o)?u.none():t(o,n,r))(0,Q,e,t,o),predicate:G,all:(e,t)=>{const o=a(t)?t:m;let n=e.dom;const r=[];for(;null!==n.parentNode&&void 0!==n.parentNode;){const e=n.parentNode,t=M.fromDom(e);if(r.push(t),!0===o(t))break;n=e}return r}}),down:l({selector:(e,t)=>((e,t)=>{const o=void 0===t?document:t.dom;return R(o)?[]:h(o.querySelectorAll(e),M.fromDom)})(t,e),predicate:_}),styles:l({get:(e,t)=>{const o=e.dom,n=window.getComputedStyle(o).getPropertyValue(t);return""!==n||F(e)?n:H(o,t)},getRaw:(e,t)=>{const o=e.dom,n=H(o,t);return u.from(n).filter((e=>e.length>0))},set:(e,t,o)=>{((e,t,o)=>{if(!n(o))throw console.error("Invalid call to CSS.set. Property ",t,":: Value ",o,":: Element ",e),new Error("CSS value must be a string: "+o);$(e)&&e.style.setProperty(t,o)})(e.dom,t,o)},remove:(e,t)=>{((e,t)=>{$(e)&&e.style.removeProperty(t)})(e.dom,t),((e,t,o=c)=>e.exists((e=>o(e,t))))(D(e,"style").map(B),"")&&E(e,"style")}}),attrs:l({get:x,set:(e,t,o)=>{S(e.dom,t,o)},remove:E,copyTo:(e,t)=>{const o=(n=e.dom.attributes,r=(e,t)=>(e[t.name]=t.value,e),s={},f(n,((e,t)=>{s=r(s,e)})),s);var n,r,s;((e,t)=>{const o=e.dom;((e,t)=>{const o=y(e);for(let n=0,r=o.length;n{S(o,t,e)}))})(t,o)}}),insert:l({before:z,after:K,afterAll:Y,append:U,appendAll:(e,t)=>{f(t,(t=>{U(e,t)}))},prepend:(e,t)=>{(e=>(e=>{const t=e.dom.childNodes;return u.from(t[0]).map(M.fromDom)})(e))(e).fold((()=>{U(e,t)}),(o=>{e.dom.insertBefore(t.dom,o.dom)}))},wrap:(e,t)=>{z(e,t),U(t,e)}}),remove:l({unwrap:e=>{const t=j(e);t.length>0&&Y(e,t),J(e)},remove:J}),create:l({nu:M.fromTag,clone:e=>M.fromDom(e.dom.cloneNode(!1)),text:M.fromText}),query:l({comparePosition:(e,t)=>e.dom.compareDocumentPosition(t.dom),prevSibling:e=>u.from(e.dom.previousSibling).map(M.fromDom),nextSibling:I}),property:l({children:j,name:v,parent:V,document:e=>{return(t=e,A(t)?t:M.fromDom(t.dom.ownerDocument)).dom;var t},isText:T,isComment:e=>8===w(e)||"#comment"===v(e),isElement:N,isSpecial:e=>{const t=v(e);return p(["script","noscript","iframe","noframes","noembed","title","style","textarea","xmp"],t)},getLanguage:e=>N(e)?D(e,"lang"):u.none(),getText:e=>X.get(e),setText:(e,t)=>X.set(e,t),isBoundary:e=>!!N(e)&&("body"===v(e)||p(Z,v(e))),isEmptyTag:e=>!!N(e)&&p(["br","img","hr","input"],v(e)),isNonEditable:e=>N(e)&&"false"===x(e,"contenteditable")}),eq:(e,t)=>e.dom===t.dom,is:k},ie="details",ae="mce-accordion",de="mce-accordion-summary",le="mce-accordion-body",ce="div";var me=tinymce.util.Tools.resolve("tinymce.util.Tools");const ue=e=>"SUMMARY"===(null==e?void 0:e.nodeName),ge=e=>"DETAILS"===(null==e?void 0:e.nodeName),pe=e=>e.hasAttribute("open"),he=e=>{const t=e.selection.getNode();return ue(t)||Boolean(e.dom.getParent(t,ue))},fe=e=>!he(e)&&e.dom.isEditable(e.selection.getNode())&&!e.mode.isReadOnly(),ye=e=>u.from(e.dom.getParent(e.selection.getNode(),ge)),ve=e=>(e.innerHTML='
    ',e),we=e=>ve(e.dom.create("p")),be=e=>t=>{((e,t)=>{if(ue(null==t?void 0:t.lastChild)){const o=we(e);t.appendChild(o),e.selection.setCursorLocation(o,0)}})(e,t),((e,t)=>{if(!ue(null==t?void 0:t.firstChild)){const o=(e=>ve(e.dom.create("summary")))(e);t.prepend(o),e.selection.setCursorLocation(o,0)}})(e,t)},Ne=e=>{if(!fe(e))return;const o=M.fromDom(e.getBody()),n=(e=>{const o=(new Date).getTime(),n=Math.floor(window.crypto.getRandomValues(new Uint32Array(1))[0]/4294967295*1e9);return t++,e+"_"+n+t+String(o)})("acc"),r=e.dom.encode(e.selection.getRng().toString()||e.translate("Accordion summary...")),s=e.dom.encode(e.translate("Accordion body...")),i=`${r}`,a=`<${ce} class="${le}">

    ${s}

    `;e.undoManager.transact((()=>{e.insertContent([`
    `,i,a,"
    "].join("")),W(o,`[data-mce-id="${n}"]`).each((t=>{E(t,"data-mce-id"),W(t,"summary").each((t=>{const o=e.dom.createRng(),n=re(se,t);o.setStart(n.element.dom,n.offset),o.setEnd(n.element.dom,n.offset),e.selection.setRng(o)}))}))}))},Te=(e,t)=>{const o=null!=t?t:!pe(e);return o?e.setAttribute("open","open"):e.removeAttribute("open"),o},Ae=e=>{e.addCommand("InsertAccordion",(()=>Ne(e))),e.addCommand("ToggleAccordion",((t,o)=>((e,t)=>{ye(e).each((o=>{((e,t,o)=>{e.dispatch("ToggledAccordion",{element:t,state:o})})(e,o,Te(o,t))}))})(e,o))),e.addCommand("ToggleAllAccordions",((t,o)=>((e,t)=>{const o=Array.from(e.getBody().querySelectorAll("details"));0!==o.length&&(f(o,(e=>Te(e,null!=t?t:!pe(e)))),((e,t,o)=>{e.dispatch("ToggledAllAccordions",{elements:t,state:o})})(e,o,t))})(e,o))),e.addCommand("RemoveAccordion",(()=>(e=>{e.mode.isReadOnly()||ye(e).each((t=>{const{nextSibling:o}=t;o?(e.selection.select(o,!0),e.selection.collapse(!0)):((e,t)=>{const o=we(e);t.insertAdjacentElement("afterend",o),e.selection.setCursorLocation(o,0)})(e,t),t.remove()}))})(e)))};var Ce=tinymce.util.Tools.resolve("tinymce.html.Node");const Se=e=>{var t,o;return null!==(o=null===(t=e.attr("class"))||void 0===t?void 0:t.split(" "))&&void 0!==o?o:[]},xe=(e,t)=>{const o=new Set([...Se(e),...t]),n=Array.from(o);n.length>0&&e.attr("class",n.join(" "))},De=(e,t)=>{const o=(e=>{const o=[];for(let r=0,s=e.length;r0?o.join(" "):null)},Ee=e=>e.name===ie&&p(Se(e),ae),Oe=e=>{const t=e.children();let o,n;const r=[];for(let e=0;e{const t=new Ce("br",1);t.attr("data-mce-bogus","1"),e.empty(),e.append(t)};var Pe=tinymce.util.Tools.resolve("tinymce.util.VK");const Re=e=>{(e=>{e.on("keydown",(t=>{(!t.shiftKey&&t.keyCode===Pe.ENTER&&he(e)||(e=>{const t=e.selection.getRng();return ge(t.startContainer)&&t.collapsed&&0===t.startOffset})(e))&&(t.preventDefault(),e.execCommand("ToggleAccordion"))}))})(e),e.on("ExecCommand",(t=>{const o=t.command.toLowerCase();"delete"!==o&&"forwarddelete"!==o||!(e=>ye(e).isSome())(e)||(e=>{me.each(me.grep(e.dom.select("details",e.getBody())),be(e))})(e)}))};var ke=tinymce.util.Tools.resolve("tinymce.Env");const Be=e=>t=>{const o=()=>t.setEnabled(fe(e));return e.on("NodeChange",o),()=>e.off("NodeChange",o)};e.add("accordion",(e=>{(e=>{const t=()=>e.execCommand("InsertAccordion");e.ui.registry.addButton("accordion",{icon:"accordion",tooltip:"Insert accordion",onSetup:Be(e),onAction:t}),e.ui.registry.addMenuItem("accordion",{icon:"accordion",text:"Accordion",onSetup:Be(e),onAction:t}),e.ui.registry.addToggleButton("accordiontoggle",{icon:"accordion-toggle",tooltip:"Toggle accordion",onAction:()=>e.execCommand("ToggleAccordion")}),e.ui.registry.addToggleButton("accordionremove",{icon:"remove",tooltip:"Delete accordion",onAction:()=>e.execCommand("RemoveAccordion")}),e.ui.registry.addContextToolbar("accordion",{predicate:t=>e.dom.is(t,"details")&&e.getBody().contains(t)&&e.dom.isEditable(t.parentNode),items:"accordiontoggle accordionremove",scope:"node",position:"node"})})(e),Ae(e),Re(e),(e=>{e.on("PreInit",(()=>{const{serializer:t,parser:o}=e;o.addNodeFilter(ie,(e=>{for(let t=0;t0)for(let e=0;e{const t=new Set([de]);for(let o=0;o{ke.browser.isSafari()&&e.on("click",(t=>{if(ue(t.target)){const o=t.target,n=e.selection.getRng();n.collapsed&&n.startContainer===o.parentNode&&0===n.startOffset&&e.selection.setCursorLocation(o,0)}}))})(e)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/advlist/plugin.min.js b/src/main/webapp/content/tinymce/plugins/advlist/plugin.min.js new file mode 100644 index 0000000..781711d --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/advlist/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=(t,e,s)=>{const r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===s?null:{"list-style-type":s})},s=t=>e=>e.options.get(t),r=s("advlist_number_styles"),n=s("advlist_bullet_styles"),l=t=>null==t,i=t=>!l(t);class o{constructor(t,e){this.tag=t,this.value=e}static some(t){return new o(!0,t)}static none(){return o.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?o.some(t(this.value)):o.none()}bind(t){return this.tag?t(this.value):o.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:o.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return i(t)?o.some(t):o.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}o.singletonNone=new o(!1);const a=Array.prototype.indexOf,u=Object.keys;var d=tinymce.util.Tools.resolve("tinymce.util.Tools");const c=t=>e=>i(e)&&t.test(e.nodeName),h=c(/^(OL|UL|DL)$/),g=c(/^(TH|TD)$/),p=t=>l(t)||"default"===t?"":t,m=(t,e)=>s=>((t,e)=>{const s=t.selection.getNode();return e({parents:t.dom.getParents(s),element:s}),t.on("NodeChange",e),()=>t.off("NodeChange",e)})(t,(r=>((t,r)=>{const n=t.selection.getStart(!0);s.setActive(((t,e,s)=>((t,e,s)=>{for(let e=0,n=t.length;ee.nodeName===s&&((t,e)=>t.dom.isChildOf(e,t.getBody()))(t,e))))(t,r,e)),s.setEnabled(!((t,e)=>{const s=t.dom.getParent(e,"ol,ul,dl");return((t,e)=>null!==e&&!t.dom.isEditable(e))(t,s)||!t.selection.isEditable()})(t,n))})(t,r.parents))),v=(t,s,r,n,l,i)=>{const c={"lower-latin":"lower-alpha","upper-latin":"upper-alpha","lower-alpha":"lower-latin","upper-alpha":"upper-latin"},h=(g=t=>{return e=i,s=t,a.call(e,s)>-1;var e,s},((t,e)=>{const s={};return((t,e)=>{const s=u(t);for(let r=0,n=s.length;r{const n=e(t,r);s[n.k]=n.v})),s})(c,((t,e)=>({k:e,v:g(t)}))));var g;t.ui.registry.addSplitButton(s,{tooltip:r,icon:"OL"===l?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:t=>{t(d.map(i,(t=>{const e="OL"===l?"num":"bull",s="disc"===t||"decimal"===t?"default":t,r=p(t),n=(t=>t.replace(/\-/g," ").replace(/\b\w/g,(t=>t.toUpperCase())))(t);return{type:"choiceitem",value:r,icon:"list-"+e+"-"+s,text:n}})))},onAction:()=>t.execCommand(n),onItemAction:(s,r)=>{e(t,l,r)},select:e=>{const s=(t=>{const e=t.dom.getParent(t.selection.getNode(),"ol,ul"),s=t.dom.getStyle(e,"listStyleType");return o.from(s)})(t);return s.exists((t=>e===t||c[t]===e&&!h[e]))},onSetup:m(t,l)})},y=(t,s,r,n,l,i)=>{i.length>1?v(t,s,r,n,l,i):((t,s,r,n,l,i)=>{t.ui.registry.addToggleButton(s,{active:!1,tooltip:r,icon:"OL"===l?"ordered-list":"unordered-list",onSetup:m(t,l),onAction:()=>t.queryCommandState(n)||""===i?t.execCommand(n):e(t,l,i)})})(t,s,r,n,l,p(i[0]))};t.add("advlist",(t=>{t.hasPlugin("lists")?((t=>{const e=t.options.register;e("advlist_number_styles",{processor:"string[]",default:"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman".split(",")}),e("advlist_bullet_styles",{processor:"string[]",default:"default,circle,square".split(",")})})(t),(t=>{y(t,"numlist","Numbered list","InsertOrderedList","OL",r(t)),y(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))})(t),(t=>{t.addCommand("ApplyUnorderedListStyle",((s,r)=>{e(t,"UL",r["list-style-type"])})),t.addCommand("ApplyOrderedListStyle",((s,r)=>{e(t,"OL",r["list-style-type"])}))})(t)):console.error("Please use the Lists plugin together with the List Styles plugin.")}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/anchor/plugin.min.js b/src/main/webapp/content/tinymce/plugins/anchor/plugin.min.js new file mode 100644 index 0000000..3c944ed --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/anchor/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=e=>e.options.get("allow_html_in_named_anchor");const a="a:not([href])",r=e=>!e,i=e=>e.getAttribute("id")||e.getAttribute("name")||"",l=e=>(e=>"a"===e.nodeName.toLowerCase())(e)&&!e.getAttribute("href")&&""!==i(e),s=e=>e.dom.getParent(e.selection.getStart(),a),d=(e,a)=>{const r=s(e);r?((e,t,o)=>{o.removeAttribute("name"),o.id=t,e.addVisual(),e.undoManager.add()})(e,a,r):((e,a)=>{e.undoManager.transact((()=>{n(e)||e.selection.collapse(!0),e.selection.isCollapsed()?e.insertContent(e.dom.createHTML("a",{id:a})):((e=>{const n=e.dom;t(n).walk(e.selection.getRng(),(e=>{o.each(e,(e=>{var t;l(t=e)&&!t.firstChild&&n.remove(e,!1)}))}))})(e),e.formatter.remove("namedAnchor",void 0,void 0,!0),e.formatter.apply("namedAnchor",{value:a}),e.addVisual())}))})(e,a),e.focus()},c=e=>(e=>r(e.attr("href"))&&!r(e.attr("id")||e.attr("name")))(e)&&!e.firstChild,m=e=>t=>{for(let o=0;ot=>{const o=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",o),o(),()=>{e.off("NodeChange",o)}};e.add("anchor",(e=>{(e=>{(0,e.options.register)("allow_html_in_named_anchor",{processor:"boolean",default:!1})})(e),(e=>{e.on("PreInit",(()=>{e.parser.addNodeFilter("a",m("false")),e.serializer.addNodeFilter("a",m(null))}))})(e),(e=>{e.addCommand("mceAnchor",(()=>{(e=>{const t=(e=>{const t=s(e);return t?i(t):""})(e);e.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:t=>{((e,t)=>/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)?(d(e,t),!0):(e.windowManager.alert("ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),!1))(e,t.getData().id)&&t.close()}})})(e)}))})(e),(e=>{const t=()=>e.execCommand("mceAnchor");e.ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:t,onSetup:t=>{const o=e.selection.selectorChangedWithUnbind("a:not([href])",t.setActive).unbind,n=u(e)(t);return()=>{o(),n()}}}),e.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:t,onSetup:u(e)})})(e),e.on("PreInit",(()=>{(e=>{e.formatter.register("namedAnchor",{inline:"a",selector:a,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:(e,t,o)=>l(e)})})(e)}))}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/autolink/plugin.min.js b/src/main/webapp/content/tinymce/plugins/autolink/plugin.min.js new file mode 100644 index 0000000..ba87c86 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/autolink/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),n=t("autolink_pattern"),o=t("link_default_target"),r=t("link_default_protocol"),a=t("allow_unsafe_link_target"),s=e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(a=o.constructor)||void 0===a?void 0:a.name)===r.name)?"string":t;var n,o,r,a})(e);const l=e=>undefined===e;const i=e=>!(e=>null==e)(e),c=Object.hasOwnProperty,d=e=>"\ufeff"===e;var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!d(t)&&n(t))return o}return-1},m=(e,t)=>{var o;const a=e.schema.getVoidElements(),s=n(e),{dom:i,selection:d}=e;if(null!==i.getParent(d.getNode(),"a[href]")||e.mode.isReadOnly())return null;const m=d.getRng(),k=u(i,(e=>{return i.isBlock(e)||(t=a,n=e.nodeName.toLowerCase(),c.call(t,n))||"false"===i.getContentEditable(e);var t,n})),{container:p,offset:y}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(m.endContainer,m.endOffset),w=null!==(o=i.getParent(p,i.isBlock))&&void 0!==o?o:i.getRoot(),h=k.backwards(p,y+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),w);if(!h)return null;let v=h.container;const _=k.backwards(h.container,h.offset,((e,t)=>{v=e;const n=g(e.data,t,f);return-1===n?n:n+1}),w),A=i.createRng();_?A.setStart(_.container,_.offset):A.setStart(v,0),A.setEnd(h.container,h.offset);const C=A.toString().replace(/\uFEFF/g,"").match(s);if(C){let t=C[0];return $="www.",(b=t).length>=4&&b.substr(0,4)===$?t=r(e)+"://"+t:((e,t,n=0,o)=>{const r=e.indexOf(t,n);return-1!==r&&(!!l(o)||r+t.length<=o)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:A,url:t}}var b,$;return null},k=(e,t)=>{const{dom:n,selection:r}=e,{rng:l,url:i}=t,c=r.getBookmark();r.setRng(l);const d="createlink",u={command:d,ui:!1,value:i};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(d,!1,i),e.dispatch("ExecCommand",u);const t=o(e);if(s(t)){const o=r.getNode();n.setAttrib(o,"target",t),"_blank"!==t||a(e)||n.setAttrib(o,"rel","noopener")}}r.moveToBookmark(c),e.nodeChanged()},p=e=>{const t=m(e,-1);i(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);i(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/autoresize/plugin.min.js b/src/main/webapp/content/tinymce/plugins/autoresize/plugin.min.js new file mode 100644 index 0000000..1f5e37f --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/autoresize/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env");const o=e=>t=>t.options.get(e),n=o("min_height"),s=o("max_height"),i=o("autoresize_overflow_padding"),r=o("autoresize_bottom_margin"),g=(e,t)=>{const o=e.getBody();o&&(o.style.overflowY=t?"":"hidden",t||(o.scrollTop=0))},l=(e,t,o,n)=>{var s;const i=parseInt(null!==(s=e.getStyle(t,o,n))&&void 0!==s?s:"",10);return isNaN(i)?0:i},a=(e,o,r,c)=>{var d;const u=e.dom,h=e.getDoc();if(!h)return;if((e=>e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen())(e))return void g(e,!0);const m=h.documentElement,f=c?c():i(e),p=null!==(d=n(e))&&void 0!==d?d:e.getElement().offsetHeight;let y=p;const S=l(u,m,"margin-top",!0),v=l(u,m,"margin-bottom",!0);let C=m.offsetHeight+S+v+f;C<0&&(C=0);const H=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight;C+H>p&&(y=C+H);const b=s(e);b&&y>b?(y=b,g(e,!0)):g(e,!1);const w=o.get();if(w.set&&(e.dom.setStyles(e.getDoc().documentElement,{"min-height":0}),e.dom.setStyles(e.getBody(),{"min-height":"inherit"})),y!==w.totalHeight&&(C-f!==w.contentHeight||!w.set)){const n=y-w.totalHeight;if(u.setStyle(e.getContainer(),"height",y+"px"),o.set({totalHeight:y,contentHeight:C,set:!0}),(e=>{e.dispatch("ResizeEditor")})(e),t.browser.isSafari()&&(t.os.isMacOS()||t.os.isiOS())){const t=e.getWin();t.scrollTo(t.pageXOffset,t.pageYOffset)}e.hasFocus()&&(e=>{if("setcontent"===(null==e?void 0:e.type.toLowerCase())){const t=e;return!0===t.selection||!0===t.paste}return!1})(r)&&e.selection.scrollIntoView(),(t.browser.isSafari()||t.browser.isChromium())&&n<0&&a(e,o,r,c)}};e.add("autoresize",(e=>{if((e=>{const t=e.options.register;t("autoresize_overflow_padding",{processor:"number",default:1}),t("autoresize_bottom_margin",{processor:"number",default:50})})(e),e.options.isSet("resize")||e.options.set("resize",!1),!e.inline){const o=(()=>{let e={totalHeight:0,contentHeight:0,set:!1};return{get:()=>e,set:t=>{e=t}}})();((e,t)=>{e.addCommand("mceAutoResize",(()=>{a(e,t)}))})(e,o),((e,o)=>{const n=()=>r(e);e.on("init",(s=>{const r=i(e),g=e.dom;g.setStyles(e.getDoc().documentElement,{height:"auto"}),t.browser.isEdge()||t.browser.isIE()?g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r,"min-height":0}):g.setStyles(e.getBody(),{paddingLeft:r,paddingRight:r}),a(e,o,s,n)})),e.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",(t=>{a(e,o,t,n)}))})(e,o)}}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/autosave/plugin.min.js b/src/main/webapp/content/tinymce/plugins/autosave/plugin.min.js new file mode 100644 index 0000000..354d42e --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/autosave/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(r=o=t,(a=String).prototype.isPrototypeOf(r)||(null===(s=o.constructor)||void 0===s?void 0:s.name)===a.name)?"string":e;var r,o,a,s})(t);const r=t=>undefined===t;var o=tinymce.util.Tools.resolve("tinymce.util.Delay"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),s=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=t=>{const e=/^(\d+)([ms]?)$/.exec(t);return(e&&e[2]?{s:1e3,m:6e4}[e[2]]:1)*parseInt(t,10)},i=t=>e=>e.options.get(t),u=i("autosave_ask_before_unload"),l=i("autosave_restore_when_empty"),c=i("autosave_interval"),d=i("autosave_retention"),m=t=>{const e=document.location;return t.options.get("autosave_prefix").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)},v=(t,e)=>{if(r(e))return t.dom.isEmpty(t.getBody());{const r=s.trim(e);if(""===r)return!0;{const e=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(e)}}},f=t=>{var e;const r=parseInt(null!==(e=a.getItem(m(t)+"time"))&&void 0!==e?e:"0",10)||0;return!((new Date).getTime()-r>d(t)&&(p(t,!1),1))},p=(t,e)=>{const r=m(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&(t=>{t.dispatch("RemoveDraft")})(t)},y=t=>{const e=m(t);!v(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),(t=>{t.dispatch("StoreDraft")})(t))},g=t=>{var e;const r=m(t);f(t)&&(t.setContent(null!==(e=a.getItem(r+"draft"))&&void 0!==e?e:"",{format:"raw"}),(t=>{t.dispatch("RestoreDraft")})(t))};var D=tinymce.util.Tools.resolve("tinymce.EditorManager");const h=t=>e=>{const r=()=>f(t)&&!t.mode.isReadOnly();e.setEnabled(r());const o=()=>e.setEnabled(r());return t.on("StoreDraft RestoreDraft RemoveDraft",o),()=>t.off("StoreDraft RestoreDraft RemoveDraft",o)};t.add("autosave",(t=>((t=>{const r=t.options.register,o=t=>{const r=e(t);return r?{value:n(t),valid:r}:{valid:!1,message:"Must be a string."}};r("autosave_ask_before_unload",{processor:"boolean",default:!0}),r("autosave_prefix",{processor:"string",default:"tinymce-autosave-{path}{query}{hash}-{id}-"}),r("autosave_restore_when_empty",{processor:"boolean",default:!1}),r("autosave_interval",{processor:o,default:"30s"}),r("autosave_retention",{processor:o,default:"20m"})})(t),(t=>{t.editorManager.on("BeforeUnload",(t=>{let e;s.each(D.get(),(t=>{t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))})),e&&(t.preventDefault(),t.returnValue=e)}))})(t),(t=>{(t=>{const e=c(t);o.setEditorInterval(t,(()=>{y(t)}),e)})(t);const e=()=>{(t=>{t.undoManager.transact((()=>{g(t),p(t)})),t.focus()})(t)};t.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)}),t.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:e,onSetup:h(t)})})(t),t.on("init",(()=>{l(t)&&t.dom.isEmpty(t.getBody())&&g(t)})),(t=>({hasDraft:()=>f(t),storeDraft:()=>y(t),restoreDraft:()=>g(t),removeDraft:e=>p(t,e),isEmpty:e=>v(t,e)}))(t))))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/charmap/plugin.min.js b/src/main/webapp/content/tinymce/plugins/charmap/plugin.min.js new file mode 100644 index 0000000..cd905e6 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/charmap/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=(e,t)=>{const r=((e,t)=>e.dispatch("insertCustomChar",{chr:t}))(e,t).chr;e.execCommand("mceInsertContent",!1,r)},r=e=>t=>e===t,a=e=>"array"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(r=a=e,(n=String).prototype.isPrototypeOf(r)||(null===(i=a.constructor)||void 0===i?void 0:i.name)===n.name)?"string":t;var r,a,n,i})(e);const n=r(null),i=r(void 0),o=e=>"function"==typeof e,s=()=>false;class l{constructor(e,t){this.tag=e,this.value=t}static some(e){return new l(!0,e)}static none(){return l.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?l.some(e(this.value)):l.none()}bind(e){return this.tag?e(this.value):l.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:l.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return null==e?l.none():l.some(e)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}l.singletonNone=new l(!1);const c=Array.prototype.push,u=(e,t)=>{const r=e.length,a=new Array(r);for(let n=0;nt=>t.options.get(e),m=h("charmap"),p=h("charmap_append"),d=g.isArray,f="User Defined",y=e=>{return d(e)?(t=e,g.grep(t,(e=>d(e)&&2===e.length))):"function"==typeof e?e():[];var t},b=e=>{const t=((e,t)=>{const r=m(e);r&&(t=[{name:f,characters:y(r)}]);const a=p(e);if(a){const e=g.grep(t,(e=>e.name===f));return e.length?(e[0].characters=[...e[0].characters,...y(a)],t):t.concat({name:f,characters:y(a)})}return t})(e,[{name:"Currency",characters:[[36,"dollar sign"],[162,"cent sign"],[8364,"euro sign"],[163,"pound sign"],[165,"yen sign"],[164,"currency sign"],[8352,"euro-currency sign"],[8353,"colon sign"],[8354,"cruzeiro sign"],[8355,"french franc sign"],[8356,"lira sign"],[8357,"mill sign"],[8358,"naira sign"],[8359,"peseta sign"],[8360,"rupee sign"],[8361,"won sign"],[8362,"new sheqel sign"],[8363,"dong sign"],[8365,"kip sign"],[8366,"tugrik sign"],[8367,"drachma sign"],[8368,"german penny symbol"],[8369,"peso sign"],[8370,"guarani sign"],[8371,"austral sign"],[8372,"hryvnia sign"],[8373,"cedi sign"],[8374,"livre tournois sign"],[8375,"spesmilo sign"],[8376,"tenge sign"],[8377,"indian rupee sign"],[8378,"turkish lira sign"],[8379,"nordic mark sign"],[8380,"manat sign"],[8381,"ruble sign"],[20870,"yen character"],[20803,"yuan character"],[22291,"yuan character, in hong kong and taiwan"],[22278,"yen/yuan character variant one"]]},{name:"Text",characters:[[169,"copyright sign"],[174,"registered sign"],[8482,"trade mark sign"],[8240,"per mille sign"],[181,"micro sign"],[183,"middle dot"],[8226,"bullet"],[8230,"three dot leader"],[8242,"minutes / feet"],[8243,"seconds / inches"],[167,"section sign"],[182,"paragraph sign"],[223,"sharp s / ess-zed"]]},{name:"Quotations",characters:[[8249,"single left-pointing angle quotation mark"],[8250,"single right-pointing angle quotation mark"],[171,"left pointing guillemet"],[187,"right pointing guillemet"],[8216,"left single quotation mark"],[8217,"right single quotation mark"],[8220,"left double quotation mark"],[8221,"right double quotation mark"],[8218,"single low-9 quotation mark"],[8222,"double low-9 quotation mark"],[60,"less-than sign"],[62,"greater-than sign"],[8804,"less-than or equal to"],[8805,"greater-than or equal to"],[8211,"en dash"],[8212,"em dash"],[175,"macron"],[8254,"overline"],[164,"currency sign"],[166,"broken bar"],[168,"diaeresis"],[161,"inverted exclamation mark"],[191,"turned question mark"],[710,"circumflex accent"],[732,"small tilde"],[176,"degree sign"],[8722,"minus sign"],[177,"plus-minus sign"],[247,"division sign"],[8260,"fraction slash"],[215,"multiplication sign"],[185,"superscript one"],[178,"superscript two"],[179,"superscript three"],[188,"fraction one quarter"],[189,"fraction one half"],[190,"fraction three quarters"]]},{name:"Mathematical",characters:[[402,"function / florin"],[8747,"integral"],[8721,"n-ary sumation"],[8734,"infinity"],[8730,"square root"],[8764,"similar to"],[8773,"approximately equal to"],[8776,"almost equal to"],[8800,"not equal to"],[8801,"identical to"],[8712,"element of"],[8713,"not an element of"],[8715,"contains as member"],[8719,"n-ary product"],[8743,"logical and"],[8744,"logical or"],[172,"not sign"],[8745,"intersection"],[8746,"union"],[8706,"partial differential"],[8704,"for all"],[8707,"there exists"],[8709,"diameter"],[8711,"backward difference"],[8727,"asterisk operator"],[8733,"proportional to"],[8736,"angle"]]},{name:"Extended Latin",characters:[[192,"A - grave"],[193,"A - acute"],[194,"A - circumflex"],[195,"A - tilde"],[196,"A - diaeresis"],[197,"A - ring above"],[256,"A - macron"],[198,"ligature AE"],[199,"C - cedilla"],[200,"E - grave"],[201,"E - acute"],[202,"E - circumflex"],[203,"E - diaeresis"],[274,"E - macron"],[204,"I - grave"],[205,"I - acute"],[206,"I - circumflex"],[207,"I - diaeresis"],[298,"I - macron"],[208,"ETH"],[209,"N - tilde"],[210,"O - grave"],[211,"O - acute"],[212,"O - circumflex"],[213,"O - tilde"],[214,"O - diaeresis"],[216,"O - slash"],[332,"O - macron"],[338,"ligature OE"],[352,"S - caron"],[217,"U - grave"],[218,"U - acute"],[219,"U - circumflex"],[220,"U - diaeresis"],[362,"U - macron"],[221,"Y - acute"],[376,"Y - diaeresis"],[562,"Y - macron"],[222,"THORN"],[224,"a - grave"],[225,"a - acute"],[226,"a - circumflex"],[227,"a - tilde"],[228,"a - diaeresis"],[229,"a - ring above"],[257,"a - macron"],[230,"ligature ae"],[231,"c - cedilla"],[232,"e - grave"],[233,"e - acute"],[234,"e - circumflex"],[235,"e - diaeresis"],[275,"e - macron"],[236,"i - grave"],[237,"i - acute"],[238,"i - circumflex"],[239,"i - diaeresis"],[299,"i - macron"],[240,"eth"],[241,"n - tilde"],[242,"o - grave"],[243,"o - acute"],[244,"o - circumflex"],[245,"o - tilde"],[246,"o - diaeresis"],[248,"o slash"],[333,"o macron"],[339,"ligature oe"],[353,"s - caron"],[249,"u - grave"],[250,"u - acute"],[251,"u - circumflex"],[252,"u - diaeresis"],[363,"u - macron"],[253,"y - acute"],[254,"thorn"],[255,"y - diaeresis"],[563,"y - macron"],[913,"Alpha"],[914,"Beta"],[915,"Gamma"],[916,"Delta"],[917,"Epsilon"],[918,"Zeta"],[919,"Eta"],[920,"Theta"],[921,"Iota"],[922,"Kappa"],[923,"Lambda"],[924,"Mu"],[925,"Nu"],[926,"Xi"],[927,"Omicron"],[928,"Pi"],[929,"Rho"],[931,"Sigma"],[932,"Tau"],[933,"Upsilon"],[934,"Phi"],[935,"Chi"],[936,"Psi"],[937,"Omega"],[945,"alpha"],[946,"beta"],[947,"gamma"],[948,"delta"],[949,"epsilon"],[950,"zeta"],[951,"eta"],[952,"theta"],[953,"iota"],[954,"kappa"],[955,"lambda"],[956,"mu"],[957,"nu"],[958,"xi"],[959,"omicron"],[960,"pi"],[961,"rho"],[962,"final sigma"],[963,"sigma"],[964,"tau"],[965,"upsilon"],[966,"phi"],[967,"chi"],[968,"psi"],[969,"omega"]]},{name:"Symbols",characters:[[8501,"alef symbol"],[982,"pi symbol"],[8476,"real part symbol"],[978,"upsilon - hook symbol"],[8472,"Weierstrass p"],[8465,"imaginary part"]]},{name:"Arrows",characters:[[8592,"leftwards arrow"],[8593,"upwards arrow"],[8594,"rightwards arrow"],[8595,"downwards arrow"],[8596,"left right arrow"],[8629,"carriage return"],[8656,"leftwards double arrow"],[8657,"upwards double arrow"],[8658,"rightwards double arrow"],[8659,"downwards double arrow"],[8660,"left right double arrow"],[8756,"therefore"],[8834,"subset of"],[8835,"superset of"],[8836,"not a subset of"],[8838,"subset of or equal to"],[8839,"superset of or equal to"],[8853,"circled plus"],[8855,"circled times"],[8869,"perpendicular"],[8901,"dot operator"],[8968,"left ceiling"],[8969,"right ceiling"],[8970,"left floor"],[8971,"right floor"],[9001,"left-pointing angle bracket"],[9002,"right-pointing angle bracket"],[9674,"lozenge"],[9824,"black spade suit"],[9827,"black club suit"],[9829,"black heart suit"],[9830,"black diamond suit"],[8194,"en space"],[8195,"em space"],[8201,"thin space"],[8204,"zero width non-joiner"],[8205,"zero width joiner"],[8206,"left-to-right mark"],[8207,"right-to-left mark"]]}]);return t.length>1?[{name:"All",characters:(r=t,n=e=>e.characters,(e=>{const t=[];for(let r=0,n=e.length;r{let t=e;return{get:()=>t,set:e=>{t=e}}},v=(e,t,r=0,a)=>{const n=e.indexOf(t,r);return-1!==n&&(!!i(a)||n+t.length<=a)},k=String.fromCodePoint,C=(e,t)=>{const r=[],a=t.toLowerCase();return(e=>{for(let n=0,i=e.length;n!!v(k(e).toLowerCase(),r)||v(t.toLowerCase(),r)||v(t.toLowerCase().replace(/\s+/g,""),r))((t=e[n])[0],t[1],a)&&r.push(t);var t})(e.characters),u(r,(e=>({text:e[1],value:k(e[0]),icon:k(e[0])})))},x="pattern",A=(e,r)=>{const a=()=>[{label:"Search",type:"input",name:x},{type:"collection",name:"results"}],i=1===r.length?w(f):w("All"),o=(e=>{let t=null;const r=()=>{n(t)||(clearTimeout(t),t=null)};return{cancel:r,throttle:(...a)=>{r(),t=setTimeout((()=>{t=null,e.apply(null,a)}),40)}}})((e=>{const t=e.getData().pattern;((e,t)=>{var a,n;(a=r,n=e=>e.name===i.get(),((e,t,r)=>{for(let a=0,n=e.length;a{const a=C(r,t);e.setData({results:a})}))})(e,t)})),c={title:"Special Character",size:"normal",body:1===r.length?{type:"panel",items:a()}:{type:"tabpanel",tabs:u(r,(e=>({title:e.name,name:e.name,items:a()})))},buttons:[{type:"cancel",name:"close",text:"Close",primary:!0}],initialData:{pattern:"",results:C(r[0],"")},onAction:(r,a)=>{"results"===a.name&&(t(e,a.value),r.close())},onTabChange:(e,t)=>{i.set(t.newTabName),o.throttle(e)},onChange:(e,t)=>{t.name===x&&o.throttle(e)}};e.windowManager.open(c).focus(x)},q=e=>t=>{const r=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",r),r(),()=>{e.off("NodeChange",r)}};e.add("charmap",(e=>{(e=>{const t=e.options.register,r=e=>o(e)||a(e);t("charmap",{processor:r}),t("charmap_append",{processor:r})})(e);const r=b(e);return((e,t)=>{e.addCommand("mceShowCharmap",(()=>{A(e,t)}))})(e,r),(e=>{const t=()=>e.execCommand("mceShowCharmap");e.ui.registry.addButton("charmap",{icon:"insert-character",tooltip:"Special character",onAction:t,onSetup:q(e)}),e.ui.registry.addMenuItem("charmap",{icon:"insert-character",text:"Special character...",onAction:t,onSetup:q(e)})})(e),((e,t)=>{e.ui.registry.addAutocompleter("charmap",{trigger:":",columns:"auto",minChars:2,fetch:(e,r)=>new Promise(((r,a)=>{r(C(t,e))})),onAction:(t,r,a)=>{e.selection.setRng(r),e.insertContent(a),t.hide()}})})(e,r[0]),(e=>({getCharMap:()=>b(e),insertChar:r=>{t(e,r)}}))(e)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/code/plugin.min.js b/src/main/webapp/content/tinymce/plugins/code/plugin.min.js new file mode 100644 index 0000000..54a780c --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/code/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/codesample/plugin.min.js b/src/main/webapp/content/tinymce/plugins/codesample/plugin.min.js new file mode 100644 index 0000000..2283bb9 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/codesample/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>!(e=>null==e)(e),n=()=>{};class a{constructor(e,t){this.tag=e,this.value=t}static some(e){return new a(!0,e)}static none(){return a.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?a.some(e(this.value)):a.none()}bind(e){return this.tag?e(this.value):a.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:a.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return t(e)?a.some(e):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);var s=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils");const r="undefined"!=typeof window?window:Function("return this;")(),i=function(){const e=window.Prism;window.Prism={manual:!0};var t=function(e){var t=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,a={},s={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(t){return t instanceof r?new r(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/=d.reach);x+=_.value.length,_=_.next){var F=_.value;if(t.length>e.length)return;if(!(F instanceof r)){var A,S=1;if(y){if(!(A=i(v,x,e,m))||A.index>=e.length)break;var $=A.index,z=A.index+A[0].length,E=x;for(E+=_.value.length;$>=E;)E+=(_=_.next).value.length;if(x=E-=_.value.length,_.value instanceof r)continue;for(var C=_;C!==t.tail&&(Ed.reach&&(d.reach=O);var P=_.prev;if(B&&(P=u(t,P,B),x+=B.length),c(t,P,S),_=u(t,P,new r(g,f?s.tokenize(j,f):j,w,j)),T&&u(t,_,T),S>1){var N={cause:g+","+b,reach:O};o(e,t,n,_.prev,x,N),d&&N.reach>d.reach&&(d.reach=N.reach)}}}}}}function l(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function u(e,t,n){var a=t.next,s={value:n,prev:t,next:a};return t.next=s,a.prev=s,e.length++,s}function c(e,t,n){for(var a=t.next,s=0;s"+r.content+""},!e.document)return e.addEventListener?(s.disableWorkerMessageHandler||e.addEventListener("message",(function(t){var n=JSON.parse(t.data),a=n.language,r=n.code,i=n.immediateClose;e.postMessage(s.highlight(r,s.languages[a],a)),i&&e.close()}),!1),s):s;var d=s.util.currentScript();function g(){s.manual||s.highlightAll()}if(d&&(s.filename=d.src,d.hasAttribute("data-manual")&&(s.manual=!0)),!s.manual){var p=document.readyState;"loading"===p||"interactive"===p&&d&&d.defer?document.addEventListener("DOMContentLoaded",g):window.requestAnimationFrame?window.requestAnimationFrame(g):window.setTimeout(g,16)}return s}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});return t.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,a,s,r){if(n.language===a){var i=n.tokenStack=[];n.code=n.code.replace(s,(function(e){if("function"==typeof r&&!r(e))return e;for(var s,o=i.length;-1!==n.code.indexOf(s=t(a,o));)++o;return i[o]=e,s})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,a){if(n.language===a&&n.tokenStack){n.grammar=e.languages[a];var s=0,r=Object.keys(n.tokenStack);!function i(o){for(var l=0;l=r.length);l++){var u=o[l];if("string"==typeof u||u.content&&"string"==typeof u.content){var c=r[s],d=n.tokenStack[c],g="string"==typeof u?u:u.content,p=t(a,c),b=g.indexOf(p);if(b>-1){++s;var h=g.substring(0,b),f=new e.Token(a,e.tokenize(d,n.grammar),"language-"+a,d),m=g.substring(b+p.length),y=[];h&&y.push.apply(y,i([h])),y.push(f),m&&y.push.apply(y,i([m])),"string"==typeof u?o.splice.apply(o,[l,1].concat(y)):u.content=y}}else u.content&&i(u.content)}return o}(n.tokens)}}}})}(t),t.languages.c=t.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),t.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),t.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},t.languages.c.string],char:t.languages.c.char,comment:t.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:t.languages.c}}}}),t.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete t.languages.c.boolean,function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n=/\b(?!)\w+(?:\s*\.\s*\w+)*\b/.source.replace(//g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!)\w+/.source.replace(//g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:import|module)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/(?:\s*:\s*)?|:\s*/.source.replace(//g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(t),function(e){function t(e,t){return e.replace(/<<(\d+)>>/g,(function(e,n){return"(?:"+t[+n]+")"}))}function n(e,n,a){return RegExp(t(e,n),a||"")}function a(e,t){for(var n=0;n>/g,(function(){return"(?:"+e+")"}));return e.replace(/<>/g,"[^\\s\\S]")}var s="bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void",r="class enum interface record struct",i="add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)",o="abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield";function l(e){return"\\b(?:"+e.trim().replace(/ /g,"|")+")\\b"}var u=l(r),c=RegExp(l(s+" "+r+" "+i+" "+o)),d=l(r+" "+i+" "+o),g=l(s+" "+r+" "+o),p=a(/<(?:[^<>;=+\-*/%&|^]|<>)*>/.source,2),b=a(/\((?:[^()]|<>)*\)/.source,2),h=/@?\b[A-Za-z_]\w*\b/.source,f=t(/<<0>>(?:\s*<<1>>)?/.source,[h,p]),m=t(/(?!<<0>>)<<1>>(?:\s*\.\s*<<1>>)*/.source,[d,f]),y=/\[\s*(?:,\s*)*\]/.source,w=t(/<<0>>(?:\s*(?:\?\s*)?<<1>>)*(?:\s*\?)?/.source,[m,y]),k=t(/[^,()<>[\];=+\-*/%&|^]|<<0>>|<<1>>|<<2>>/.source,[p,b,y]),v=t(/\(<<0>>+(?:,<<0>>+)+\)/.source,[k]),_=t(/(?:<<0>>|<<1>>)(?:\s*(?:\?\s*)?<<2>>)*(?:\s*\?)?/.source,[v,m,y]),x={keyword:c,punctuation:/[<>()?,.:[\]]/},F=/'(?:[^\r\n'\\]|\\.|\\[Uux][\da-fA-F]{1,8})'/.source,A=/"(?:\\.|[^\\"\r\n])*"/.source,S=/@"(?:""|\\[\s\S]|[^\\"])*"(?!")/.source;e.languages.csharp=e.languages.extend("clike",{string:[{pattern:n(/(^|[^$\\])<<0>>/.source,[S]),lookbehind:!0,greedy:!0},{pattern:n(/(^|[^@$\\])<<0>>/.source,[A]),lookbehind:!0,greedy:!0}],"class-name":[{pattern:n(/(\busing\s+static\s+)<<0>>(?=\s*;)/.source,[m]),lookbehind:!0,inside:x},{pattern:n(/(\busing\s+<<0>>\s*=\s*)<<1>>(?=\s*;)/.source,[h,_]),lookbehind:!0,inside:x},{pattern:n(/(\busing\s+)<<0>>(?=\s*=)/.source,[h]),lookbehind:!0},{pattern:n(/(\b<<0>>\s+)<<1>>/.source,[u,f]),lookbehind:!0,inside:x},{pattern:n(/(\bcatch\s*\(\s*)<<0>>/.source,[m]),lookbehind:!0,inside:x},{pattern:n(/(\bwhere\s+)<<0>>/.source,[h]),lookbehind:!0},{pattern:n(/(\b(?:is(?:\s+not)?|as)\s+)<<0>>/.source,[w]),lookbehind:!0,inside:x},{pattern:n(/\b<<0>>(?=\s+(?!<<1>>|with\s*\{)<<2>>(?:\s*[=,;:{)\]]|\s+(?:in|when)\b))/.source,[_,g,h]),inside:x}],keyword:c,number:/(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:[dflmu]|lu|ul)?\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\?\.?|::|[{}[\];(),.:]/}),e.languages.insertBefore("csharp","number",{range:{pattern:/\.\./,alias:"operator"}}),e.languages.insertBefore("csharp","punctuation",{"named-parameter":{pattern:n(/([(,]\s*)<<0>>(?=\s*:)/.source,[h]),lookbehind:!0,alias:"punctuation"}}),e.languages.insertBefore("csharp","class-name",{namespace:{pattern:n(/(\b(?:namespace|using)\s+)<<0>>(?:\s*\.\s*<<0>>)*(?=\s*[;{])/.source,[h]),lookbehind:!0,inside:{punctuation:/\./}},"type-expression":{pattern:n(/(\b(?:default|sizeof|typeof)\s*\(\s*(?!\s))(?:[^()\s]|\s(?!\s)|<<0>>)*(?=\s*\))/.source,[b]),lookbehind:!0,alias:"class-name",inside:x},"return-type":{pattern:n(/<<0>>(?=\s+(?:<<1>>\s*(?:=>|[({]|\.\s*this\s*\[)|this\s*\[))/.source,[_,m]),inside:x,alias:"class-name"},"constructor-invocation":{pattern:n(/(\bnew\s+)<<0>>(?=\s*[[({])/.source,[_]),lookbehind:!0,inside:x,alias:"class-name"},"generic-method":{pattern:n(/<<0>>\s*<<1>>(?=\s*\()/.source,[h,p]),inside:{function:n(/^<<0>>/.source,[h]),generic:{pattern:RegExp(p),alias:"class-name",inside:x}}},"type-list":{pattern:n(/\b((?:<<0>>\s+<<1>>|record\s+<<1>>\s*<<5>>|where\s+<<2>>)\s*:\s*)(?:<<3>>|<<4>>|<<1>>\s*<<5>>|<<6>>)(?:\s*,\s*(?:<<3>>|<<4>>|<<6>>))*(?=\s*(?:where|[{;]|=>|$))/.source,[u,f,h,_,c.source,b,/\bnew\s*\(\s*\)/.source]),lookbehind:!0,inside:{"record-arguments":{pattern:n(/(^(?!new\s*\()<<0>>\s*)<<1>>/.source,[f,b]),lookbehind:!0,greedy:!0,inside:e.languages.csharp},keyword:c,"class-name":{pattern:RegExp(_),greedy:!0,inside:x},punctuation:/[,()]/}},preprocessor:{pattern:/(^[\t ]*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}});var $=A+"|"+F,z=t(/\/(?![*/])|\/\/[^\r\n]*[\r\n]|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>/.source,[$]),E=a(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[z]),2),C=/\b(?:assembly|event|field|method|module|param|property|return|type)\b/.source,j=t(/<<0>>(?:\s*\(<<1>>*\))?/.source,[m,E]);e.languages.insertBefore("csharp","class-name",{attribute:{pattern:n(/((?:^|[^\s\w>)?])\s*\[\s*)(?:<<0>>\s*:\s*)?<<1>>(?:\s*,\s*<<1>>)*(?=\s*\])/.source,[C,j]),lookbehind:!0,greedy:!0,inside:{target:{pattern:n(/^<<0>>(?=\s*:)/.source,[C]),alias:"keyword"},"attribute-arguments":{pattern:n(/\(<<0>>*\)/.source,[E]),inside:e.languages.csharp},"class-name":{pattern:RegExp(m),inside:{punctuation:/\./}},punctuation:/[:,]/}}});var B=/:[^}\r\n]+/.source,T=a(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[z]),2),O=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[T,B]),P=a(t(/[^"'/()]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>|\(<>*\)/.source,[$]),2),N=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[P,B]);function R(t,a){return{interpolation:{pattern:n(/((?:^|[^{])(?:\{\{)*)<<0>>/.source,[t]),lookbehind:!0,inside:{"format-string":{pattern:n(/(^\{(?:(?![}:])<<0>>)*)<<1>>(?=\}$)/.source,[a,B]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\{|\}$/,expression:{pattern:/[\s\S]+/,alias:"language-csharp",inside:e.languages.csharp}}},string:/[\s\S]+/}}e.languages.insertBefore("csharp","string",{"interpolation-string":[{pattern:n(/(^|[^\\])(?:\$@|@\$)"(?:""|\\[\s\S]|\{\{|<<0>>|[^\\{"])*"/.source,[O]),lookbehind:!0,greedy:!0,inside:R(O,T)},{pattern:n(/(^|[^@\\])\$"(?:\\.|\{\{|<<0>>|[^\\"{])*"/.source,[N]),lookbehind:!0,greedy:!0,inside:R(N,P)}],char:{pattern:RegExp(F),greedy:!0}}),e.languages.dotnet=e.languages.cs=e.languages.csharp}(t),function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+t.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),n.tag.addAttribute("style","css"))}(t),function(e){var t=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,n=/(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,a={pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[a,{pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z]\w*(?=\s+\w+\s*[;,=()]|\s*(?:\[[\s,]*\]\s*)?::\s*new\b)/.source),lookbehind:!0,inside:a.inside},{pattern:RegExp(/(\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\s+)/.source+n+/[A-Z]\w*\b/.source),lookbehind:!0,inside:a.inside}],keyword:t,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":a,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp(/(\bimport\s+)/.source+n+/(?:[A-Z]\w*|\*)(?=\s*;)/.source),lookbehind:!0,inside:{namespace:a.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp(/(\bimport\s+static\s+)/.source+n+/(?:\w+|\*)(?=\s*;)/.source),lookbehind:!0,alias:"static",inside:{namespace:a.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp(/(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?!)[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace(//g,(function(){return t.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(t),t.languages.javascript=t.languages.extend("clike",{"class-name":[t.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),t.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,t.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:t.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:t.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:t.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:t.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:t.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),t.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:t.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),t.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),t.languages.markup&&(t.languages.markup.tag.addInlined("script","javascript"),t.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),t.languages.js=t.languages.javascript,t.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},t.languages.markup.tag.inside["attr-value"].inside.entity=t.languages.markup.entity,t.languages.markup.doctype.inside["internal-subset"].inside=t.languages.markup,t.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))})),Object.defineProperty(t.languages.markup.tag,"addInlined",{value:function(e,n){var a={};a["language-"+n]={pattern:/(^$)/i,lookbehind:!0,inside:t.languages[n]},a.cdata=/^$/i;var s={"included-cdata":{pattern://i,inside:a}};s["language-"+n]={pattern:/[\s\S]+/,inside:t.languages[n]};var r={};r[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:s},t.languages.insertBefore("markup","cdata",r)}}),Object.defineProperty(t.languages.markup.tag,"addAttribute",{value:function(e,n){t.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[n,"language-"+n],inside:t.languages[n]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),t.languages.html=t.languages.markup,t.languages.mathml=t.languages.markup,t.languages.svg=t.languages.markup,t.languages.xml=t.languages.extend("markup",{}),t.languages.ssml=t.languages.xml,t.languages.atom=t.languages.xml,t.languages.rss=t.languages.xml,function(e){var t=/\/\*[\s\S]*?\*\/|\/\/.*|#(?!\[).*/,n=[{pattern:/\b(?:false|true)\b/i,alias:"boolean"},{pattern:/(::\s*)\b[a-z_]\w*\b(?!\s*\()/i,greedy:!0,lookbehind:!0},{pattern:/(\b(?:case|const)\s+)\b[a-z_]\w*(?=\s*[;=])/i,greedy:!0,lookbehind:!0},/\b(?:null)\b/i,/\b[A-Z_][A-Z0-9_]*\b(?!\s*\()/],a=/\b0b[01]+(?:_[01]+)*\b|\b0o[0-7]+(?:_[0-7]+)*\b|\b0x[\da-f]+(?:_[\da-f]+)*\b|(?:\b\d+(?:_\d+)*\.?(?:\d+(?:_\d+)*)?|\B\.\d+)(?:e[+-]?\d+)?/i,s=/|\?\?=?|\.{3}|\??->|[!=]=?=?|::|\*\*=?|--|\+\+|&&|\|\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,r=/[{}\[\](),:;]/;e.languages.php={delimiter:{pattern:/\?>$|^<\?(?:php(?=\s)|=)?/i,alias:"important"},comment:t,variable:/\$+(?:\w+\b|(?=\{))/,package:{pattern:/(namespace\s+|use\s+(?:function\s+)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,lookbehind:!0,inside:{punctuation:/\\/}},"class-name-definition":{pattern:/(\b(?:class|enum|interface|trait)\s+)\b[a-z_]\w*(?!\\)\b/i,lookbehind:!0,alias:"class-name"},"function-definition":{pattern:/(\bfunction\s+)[a-z_]\w*(?=\s*\()/i,lookbehind:!0,alias:"function"},keyword:[{pattern:/(\(\s*)\b(?:array|bool|boolean|float|int|integer|object|string)\b(?=\s*\))/i,alias:"type-casting",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|object|self|static|string)\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|never|object|self|static|string|void)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/\b(?:array(?!\s*\()|bool|float|int|iterable|mixed|object|string|void)\b/i,alias:"type-declaration",greedy:!0},{pattern:/(\|\s*)(?:false|null)\b|\b(?:false|null)(?=\s*\|)/i,alias:"type-declaration",greedy:!0,lookbehind:!0},{pattern:/\b(?:parent|self|static)(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(\byield\s+)from\b/i,lookbehind:!0},/\bclass\b/i,{pattern:/((?:^|[^\s>:]|(?:^|[^-])>|(?:^|[^:]):)\s*)\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\b/i,lookbehind:!0}],"argument-name":{pattern:/([(,]\s*)\b[a-z_]\w*(?=\s*:(?!:))/i,lookbehind:!0},"class-name":[{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self|\s+static))\s+|\bcatch\s*\()\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/(\|\s*)\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/\b[a-z_]\w*(?!\\)\b(?=\s*\|)/i,greedy:!0},{pattern:/(\|\s*)(?:\\?\b[a-z_]\w*)+\b/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(?:\\?\b[a-z_]\w*)+\b(?=\s*\|)/i,alias:"class-name-fully-qualified",greedy:!0,inside:{punctuation:/\\/}},{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self\b|\s+static\b))\s+|\bcatch\s*\()(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*\$)/i,alias:"type-declaration",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-declaration"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*::)/i,alias:["class-name-fully-qualified","static-context"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/([(,?]\s*)[a-z_]\w*(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-hint"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b[a-z_]\w*(?!\\)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:["class-name-fully-qualified","return-type"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,function:{pattern:/(^|[^\\\w])\\?[a-z_](?:[\w\\]*\w)?(?=\s*\()/i,lookbehind:!0,inside:{punctuation:/\\/}},property:{pattern:/(->\s*)\w+/,lookbehind:!0},number:a,operator:s,punctuation:r};var i={pattern:/\{\$(?:\{(?:\{[^{}]+\}|[^{}]+)\}|[^{}])+\}|(^|[^\\{])\$+(?:\w+(?:\[[^\r\n\[\]]+\]|->\w+)?)/,lookbehind:!0,inside:e.languages.php},o=[{pattern:/<<<'([^']+)'[\r\n](?:.*[\r\n])*?\1;/,alias:"nowdoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:"([^"]+)"[\r\n](?:.*[\r\n])*?\1;|([a-z_]\w*)[\r\n](?:.*[\r\n])*?\2;)/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:i}},{pattern:/`(?:\\[\s\S]|[^\\`])*`/,alias:"backtick-quoted-string",greedy:!0},{pattern:/'(?:\\[\s\S]|[^\\'])*'/,alias:"single-quoted-string",greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,alias:"double-quoted-string",greedy:!0,inside:{interpolation:i}}];e.languages.insertBefore("php","variable",{string:o,attribute:{pattern:/#\[(?:[^"'\/#]|\/(?![*/])|\/\/.*$|#(?!\[).*$|\/\*(?:[^*]|\*(?!\/))*\*\/|"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*')+\](?=\s*[a-z$#])/im,greedy:!0,inside:{"attribute-content":{pattern:/^(#\[)[\s\S]+(?=\]$)/,lookbehind:!0,inside:{comment:t,string:o,"attribute-class-name":[{pattern:/([^:]|^)\b[a-z_]\w*(?!\\)\b/i,alias:"class-name",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\?\b[a-z_]\w*)+/i,alias:["class-name","class-name-fully-qualified"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,number:a,operator:s,punctuation:r}},delimiter:{pattern:/^#\[|\]$/,alias:"punctuation"}}}}),e.hooks.add("before-tokenize",(function(t){/<\?/.test(t.code)&&e.languages["markup-templating"].buildPlaceholders(t,"php",/<\?(?:[^"'/#]|\/(?![*/])|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|(?:\/\/|#(?!\[))(?:[^?\n\r]|\?(?!>))*(?=$|\?>|[\r\n])|#\[|\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$))*?(?:\?>|$)/g)})),e.hooks.add("after-tokenize",(function(t){e.languages["markup-templating"].tokenizePlaceholders(t,"php")}))}(t),t.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},t.languages.python["string-interpolation"].inside.interpolation.inside.rest=t.languages.python,t.languages.py=t.languages.python,function(e){e.languages.ruby=e.languages.extend("clike",{comment:{pattern:/#.*|^=begin\s[\s\S]*?^=end/m,greedy:!0},"class-name":{pattern:/(\b(?:class|module)\s+|\bcatch\s+\()[\w.\\]+|\b[A-Z_]\w*(?=\s*\.\s*new\b)/,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:BEGIN|END|alias|and|begin|break|case|class|def|define_method|defined|do|each|else|elsif|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|private|protected|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\b/,operator:/\.{2,3}|&\.|===||[!=]?~|(?:&&|\|\||<<|>>|\*\*|[+\-*/%<>!^&|=])=?|[?:]/,punctuation:/[(){}[\].,;]/}),e.languages.insertBefore("ruby","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}});var t={pattern:/((?:^|[^\\])(?:\\{2})*)#\{(?:[^{}]|\{[^{}]*\})*\}/,lookbehind:!0,inside:{content:{pattern:/^(#\{)[\s\S]+(?=\}$)/,lookbehind:!0,inside:e.languages.ruby},delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"}}};delete e.languages.ruby.function;var n="(?:"+[/([^a-zA-Z0-9\s{(\[<=])(?:(?!\1)[^\\]|\\[\s\S])*\1/.source,/\((?:[^()\\]|\\[\s\S]|\((?:[^()\\]|\\[\s\S])*\))*\)/.source,/\{(?:[^{}\\]|\\[\s\S]|\{(?:[^{}\\]|\\[\s\S])*\})*\}/.source,/\[(?:[^\[\]\\]|\\[\s\S]|\[(?:[^\[\]\\]|\\[\s\S])*\])*\]/.source,/<(?:[^<>\\]|\\[\s\S]|<(?:[^<>\\]|\\[\s\S])*>)*>/.source].join("|")+")",a=/(?:"(?:\\.|[^"\\\r\n])*"|(?:\b[a-zA-Z_]\w*|[^\s\0-\x7F]+)[?!]?|\$.)/.source;e.languages.insertBefore("ruby","keyword",{"regex-literal":[{pattern:RegExp(/%r/.source+n+/[egimnosux]{0,6}/.source),greedy:!0,inside:{interpolation:t,regex:/[\s\S]+/}},{pattern:/(^|[^/])\/(?!\/)(?:\[[^\r\n\]]+\]|\\.|[^[/\\\r\n])+\/[egimnosux]{0,6}(?=\s*(?:$|[\r\n,.;})#]))/,lookbehind:!0,greedy:!0,inside:{interpolation:t,regex:/[\s\S]+/}}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:[{pattern:RegExp(/(^|[^:]):/.source+a),lookbehind:!0,greedy:!0},{pattern:RegExp(/([\r\n{(,][ \t]*)/.source+a+/(?=:(?!:))/.source),lookbehind:!0,greedy:!0}],"method-definition":{pattern:/(\bdef\s+)\w+(?:\s*\.\s*\w+)?/,lookbehind:!0,inside:{function:/\b\w+$/,keyword:/^self\b/,"class-name":/^\w+/,punctuation:/\./}}}),e.languages.insertBefore("ruby","string",{"string-literal":[{pattern:RegExp(/%[qQiIwWs]?/.source+n),greedy:!0,inside:{interpolation:t,string:/[\s\S]+/}},{pattern:/("|')(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|(?!\1)[^\\#\r\n])*\1/,greedy:!0,inside:{interpolation:t,string:/[\s\S]+/}},{pattern:/<<[-~]?([a-z_]\w*)[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?[a-z_]\w*|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?/}},interpolation:t,string:/[\s\S]+/}},{pattern:/<<[-~]?'([a-z_]\w*)'[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?'[a-z_]\w*'|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?'|'$/}},string:/[\s\S]+/}}],"command-literal":[{pattern:RegExp(/%x/.source+n),greedy:!0,inside:{interpolation:t,command:{pattern:/[\s\S]+/,alias:"string"}}},{pattern:/`(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|[^\\`#\r\n])*`/,greedy:!0,inside:{interpolation:t,command:{pattern:/[\s\S]+/,alias:"string"}}}]}),delete e.languages.ruby.string,e.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Fixnum|Float|Hash|IO|Integer|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|Stat|String|Struct|Symbol|TMS|Thread|ThreadGroup|Time|TrueClass)\b/,constant:/\b[A-Z][A-Z0-9_]*(?:[?!]|\b)/}),e.languages.rb=e.languages.ruby}(t),window.Prism=e,t}(),o=e=>t=>t.options.get(e),l=o("codesample_languages"),u=o("codesample_global_prismjs"),c=e=>r.Prism&&u(e)?r.Prism:i,d=e=>t(e)&&"PRE"===e.nodeName&&-1!==e.className.indexOf("language-"),g=e=>{const t=e.selection?e.selection.getNode():null;return d(t)?a.some(t):a.none()},p=e=>{const t=(e=>l(e)||[{text:"HTML/XML",value:"markup"},{text:"JavaScript",value:"javascript"},{text:"CSS",value:"css"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"}])(e),n=(r=t,(e=>0""),(e=>e.value));var r;const i=((e,t)=>g(e).fold((()=>t),(e=>{const n=e.className.match(/language-(\w+)/);return n?n[1]:t})))(e,n),o=(e=>g(e).bind((e=>a.from(e.textContent))).getOr(""))(e);e.windowManager.open({title:"Insert/Edit Code Sample",size:"large",body:{type:"panel",items:[{type:"listbox",name:"language",label:"Language",items:t},{type:"textarea",name:"code",label:"Code view"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{language:i,code:o},onSubmit:t=>{const n=t.getData();((e,t,n)=>{const a=e.dom;e.undoManager.transact((()=>{const r=g(e);return n=s.DOM.encode(n),r.fold((()=>{e.insertContent('
    '+n+"
    ");const s=a.select("#__new")[0];a.setAttrib(s,"id",null),e.selection.select(s)}),(s=>{a.setAttrib(s,"class","language-"+t),s.innerHTML=n,c(e).highlightElement(s),e.selection.select(s)}))}))})(e,n.language,n.code),t.close()}})},b=(h=/^\s+|\s+$/g,e=>e.replace(h,""));var h,f=tinymce.util.Tools.resolve("tinymce.util.Tools");const m=(e,t=n)=>n=>{const a=()=>{n.setEnabled(e.selection.isEditable()),t(n)};return e.on("NodeChange",a),a(),()=>{e.off("NodeChange",a)}};e.add("codesample",(e=>{(e=>{const t=e.options.register;t("codesample_languages",{processor:"object[]"}),t("codesample_global_prismjs",{processor:"boolean",default:!1})})(e),(e=>{e.on("PreProcess",(t=>{const n=e.dom,a=n.select("pre[contenteditable=false]",t.node);f.each(f.grep(a,d),(e=>{const t=e.textContent;let a;for(n.setAttrib(e,"class",b(n.getAttrib(e,"class"))),n.setAttrib(e,"contentEditable",null),n.setAttrib(e,"data-mce-highlighted",null);a=e.firstChild;)e.removeChild(a);n.add(e,"code").textContent=t}))})),e.on("SetContent",(()=>{const t=e.dom,n=f.grep(t.select("pre"),(e=>d(e)&&"true"!==t.getAttrib(e,"data-mce-highlighted")));n.length&&e.undoManager.transact((()=>{f.each(n,(n=>{var a;f.each(t.select("br",n),(n=>{t.replace(e.getDoc().createTextNode("\n"),n)})),n.innerHTML=t.encode(null!==(a=n.textContent)&&void 0!==a?a:""),c(e).highlightElement(n),t.setAttrib(n,"data-mce-highlighted",!0),n.className=b(n.className)}))}))})),e.on("PreInit",(()=>{e.parser.addNodeFilter("pre",(e=>{var t;for(let n=0,a=e.length;n{const t=()=>e.execCommand("codesample");e.ui.registry.addToggleButton("codesample",{icon:"code-sample",tooltip:"Insert/edit code sample",onAction:t,onSetup:m(e,(t=>{t.setActive((e=>{const t=e.selection.getStart();return e.dom.is(t,'pre[class*="language-"]')})(e))}))}),e.ui.registry.addMenuItem("codesample",{text:"Code sample...",icon:"code-sample",onAction:t,onSetup:m(e)})})(e),(e=>{e.addCommand("codesample",(()=>{const t=e.selection.getNode();e.selection.isCollapsed()||d(t)?p(e):e.formatter.toggle("code")}))})(e),e.on("dblclick",(t=>{d(t.target)&&p(e)}))}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/directionality/plugin.min.js b/src/main/webapp/content/tinymce/plugins/directionality/plugin.min.js new file mode 100644 index 0000000..a4f3780 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/directionality/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>e=>typeof e===t,o=t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(o=r=t,(n=String).prototype.isPrototypeOf(o)||(null===(i=r.constructor)||void 0===i?void 0:i.name)===n.name)?"string":e;var o,r,n,i})(t),r=e("boolean"),n=t=>!(t=>null==t)(t),i=e("function"),s=e("number"),l=()=>false;class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return n(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=(t,e)=>{for(let o=0,r=t.length;o{if(null==t)throw new Error("Node cannot be null or undefined");return{dom:t}},d=c,h=(t,e)=>{const o=t.dom;if(1!==o.nodeType)return!1;{const t=o;if(void 0!==t.matches)return t.matches(e);if(void 0!==t.msMatchesSelector)return t.msMatchesSelector(e);if(void 0!==t.webkitMatchesSelector)return t.webkitMatchesSelector(e);if(void 0!==t.mozMatchesSelector)return t.mozMatchesSelector(e);throw new Error("Browser lacks native selectors")}};"undefined"!=typeof window?window:Function("return this;")();const m=t=>e=>(t=>t.dom.nodeType)(e)===t,g=m(1),f=m(3),v=m(11),y=(t,e)=>{t.dom.removeAttribute(e)},p=t=>d(t.dom.host),w=t=>{const e=f(t)?t.dom.parentNode:t.dom;if(null==e||null===e.ownerDocument)return!1;const o=e.ownerDocument;return(t=>{const e=(t=>d(t.dom.getRootNode()))(t);return v(o=e)&&n(o.dom.host)?a.some(e):a.none();var o})(d(e)).fold((()=>o.body.contains(e)),(r=w,i=p,t=>r(i(t))));var r,i},b=t=>"rtl"===((t,e)=>{const o=t.dom,r=window.getComputedStyle(o).getPropertyValue(e);return""!==r||w(t)?r:((t,e)=>(t=>void 0!==t.style&&i(t.style.getPropertyValue))(t)?t.style.getPropertyValue(e):"")(o,e)})(t,"direction")?"rtl":"ltr",S=(t,e)=>(t=>((t,e)=>{const o=[];for(let r=0,n=t.length;r{const o=t.length,r=new Array(o);for(let n=0;nh(t,e))))(t),N=t=>g(t)&&"li"===t.dom.nodeName.toLowerCase();const A=(t,e,n)=>{u(e,(e=>{const c=d(e),m=N(c),f=((t,e)=>{return(e?(o=t,r="ol,ul",((t,e,o)=>{let n=t.dom;const s=i(o)?o:l;for(;n.parentNode;){n=n.parentNode;const t=d(n);if(h(t,r))return a.some(t);if(s(t))break}return a.none()})(o,0,n)):a.some(t)).getOr(t);var o,r,n})(c,m);var v;(v=f,(t=>a.from(t.dom.parentNode).map(d))(v).filter(g)).each((e=>{if(t.setStyle(f.dom,"direction",null),b(e)===n?y(f,"dir"):((t,e,n)=>{((t,e,n)=>{if(!(o(n)||r(n)||s(n)))throw console.error("Invalid call to Attribute.set. Key ",e,":: Value ",n,":: Element ",t),new Error("Attribute value was not simple");t.setAttribute(e,n+"")})(t.dom,e,n)})(f,"dir",n),b(f)!==n&&t.setStyle(f.dom,"direction",n),m){const e=S(f,"li[dir],li[style]");u(e,(e=>{y(e,"dir"),t.setStyle(e.dom,"direction",null)}))}}))}))},T=(t,e)=>{t.selection.isEditable()&&(A(t.dom,t.selection.getSelectedBlocks(),e),t.nodeChanged())},C=(t,e)=>o=>{const r=r=>{const n=d(r.element);o.setActive(b(n)===e),o.setEnabled(t.selection.isEditable())};return t.on("NodeChange",r),o.setEnabled(t.selection.isEditable()),()=>t.off("NodeChange",r)};t.add("directionality",(t=>{(t=>{t.addCommand("mceDirectionLTR",(()=>{T(t,"ltr")})),t.addCommand("mceDirectionRTL",(()=>{T(t,"rtl")}))})(t),(t=>{t.ui.registry.addToggleButton("ltr",{tooltip:"Left to right",icon:"ltr",onAction:()=>t.execCommand("mceDirectionLTR"),onSetup:C(t,"ltr")}),t.ui.registry.addToggleButton("rtl",{tooltip:"Right to left",icon:"rtl",onAction:()=>t.execCommand("mceDirectionRTL"),onSetup:C(t,"rtl")})})(t)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/emoticons/js/emojiimages.js b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojiimages.js new file mode 100644 index 0000000..6fcec71 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojiimages.js @@ -0,0 +1 @@ +window.tinymce.Resource.add("tinymce.plugins.emoticons",{100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:'💯',fitzpatrick_scale:false,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:'🔢',fitzpatrick_scale:false,category:"symbols"},grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:'😀',fitzpatrick_scale:false,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:'😬',fitzpatrick_scale:false,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:'😁',fitzpatrick_scale:false,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:'😂',fitzpatrick_scale:false,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:'🤣',fitzpatrick_scale:false,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:'🥳',fitzpatrick_scale:false,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:'😃',fitzpatrick_scale:false,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:'😄',fitzpatrick_scale:false,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:'😅',fitzpatrick_scale:false,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:'😆',fitzpatrick_scale:false,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:'😇',fitzpatrick_scale:false,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:'😉',fitzpatrick_scale:false,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:'😊',fitzpatrick_scale:false,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:'🙂',fitzpatrick_scale:false,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:'🙃',fitzpatrick_scale:false,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:'☺️',fitzpatrick_scale:false,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:'😋',fitzpatrick_scale:false,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:'😌',fitzpatrick_scale:false,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:'😍',fitzpatrick_scale:false,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:'🥰',fitzpatrick_scale:false,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:'😘',fitzpatrick_scale:false,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:'😗',fitzpatrick_scale:false,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:'😙',fitzpatrick_scale:false,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:'😚',fitzpatrick_scale:false,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:'😜',fitzpatrick_scale:false,category:"people"},zany:{keywords:["face","goofy","crazy"],char:'🤪',fitzpatrick_scale:false,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:'🤨',fitzpatrick_scale:false,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:'🧐',fitzpatrick_scale:false,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:'😝',fitzpatrick_scale:false,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:'😛',fitzpatrick_scale:false,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:'🤑',fitzpatrick_scale:false,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:'🤓',fitzpatrick_scale:false,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:'😎',fitzpatrick_scale:false,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:'🤩',fitzpatrick_scale:false,category:"people"},clown_face:{keywords:["face"],char:'🤡',fitzpatrick_scale:false,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:'🤠',fitzpatrick_scale:false,category:"people"},hugs:{keywords:["face","smile","hug"],char:'🤗',fitzpatrick_scale:false,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:'😏',fitzpatrick_scale:false,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:'😶',fitzpatrick_scale:false,category:"people"},neutral_face:{keywords:["indifference","meh",":|","neutral"],char:'😐',fitzpatrick_scale:false,category:"people"},expressionless:{keywords:["face","indifferent","-_-","meh","deadpan"],char:'😑',fitzpatrick_scale:false,category:"people"},unamused:{keywords:["indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"],char:'😒',fitzpatrick_scale:false,category:"people"},roll_eyes:{keywords:["face","eyeroll","frustrated"],char:'🙄',fitzpatrick_scale:false,category:"people"},thinking:{keywords:["face","hmmm","think","consider"],char:'🤔',fitzpatrick_scale:false,category:"people"},lying_face:{keywords:["face","lie","pinocchio"],char:'🤥',fitzpatrick_scale:false,category:"people"},hand_over_mouth:{keywords:["face","whoops","shock","surprise"],char:'🤭',fitzpatrick_scale:false,category:"people"},shushing:{keywords:["face","quiet","shhh"],char:'🤫',fitzpatrick_scale:false,category:"people"},symbols_over_mouth:{keywords:["face","swearing","cursing","cussing","profanity","expletive"],char:'🤬',fitzpatrick_scale:false,category:"people"},exploding_head:{keywords:["face","shocked","mind","blown"],char:'🤯',fitzpatrick_scale:false,category:"people"},flushed:{keywords:["face","blush","shy","flattered"],char:'😳',fitzpatrick_scale:false,category:"people"},disappointed:{keywords:["face","sad","upset","depressed",":("],char:'😞',fitzpatrick_scale:false,category:"people"},worried:{keywords:["face","concern","nervous",":("],char:'😟',fitzpatrick_scale:false,category:"people"},angry:{keywords:["mad","face","annoyed","frustrated"],char:'😠',fitzpatrick_scale:false,category:"people"},rage:{keywords:["angry","mad","hate","despise"],char:'😡',fitzpatrick_scale:false,category:"people"},pensive:{keywords:["face","sad","depressed","upset"],char:'😔',fitzpatrick_scale:false,category:"people"},confused:{keywords:["face","indifference","huh","weird","hmmm",":/"],char:'😕',fitzpatrick_scale:false,category:"people"},slightly_frowning_face:{keywords:["face","frowning","disappointed","sad","upset"],char:'🙁',fitzpatrick_scale:false,category:"people"},frowning_face:{keywords:["face","sad","upset","frown"],char:'☹',fitzpatrick_scale:false,category:"people"},persevere:{keywords:["face","sick","no","upset","oops"],char:'😣',fitzpatrick_scale:false,category:"people"},confounded:{keywords:["face","confused","sick","unwell","oops",":S"],char:'😖',fitzpatrick_scale:false,category:"people"},tired_face:{keywords:["sick","whine","upset","frustrated"],char:'😫',fitzpatrick_scale:false,category:"people"},weary:{keywords:["face","tired","sleepy","sad","frustrated","upset"],char:'😩',fitzpatrick_scale:false,category:"people"},pleading:{keywords:["face","begging","mercy"],char:'🥺',fitzpatrick_scale:false,category:"people"},triumph:{keywords:["face","gas","phew","proud","pride"],char:'😤',fitzpatrick_scale:false,category:"people"},open_mouth:{keywords:["face","surprise","impressed","wow","whoa",":O"],char:'😮',fitzpatrick_scale:false,category:"people"},scream:{keywords:["face","munch","scared","omg"],char:'😱',fitzpatrick_scale:false,category:"people"},fearful:{keywords:["face","scared","terrified","nervous","oops","huh"],char:'😨',fitzpatrick_scale:false,category:"people"},cold_sweat:{keywords:["face","nervous","sweat"],char:'😰',fitzpatrick_scale:false,category:"people"},hushed:{keywords:["face","woo","shh"],char:'😯',fitzpatrick_scale:false,category:"people"},frowning:{keywords:["face","aw","what"],char:'😦',fitzpatrick_scale:false,category:"people"},anguished:{keywords:["face","stunned","nervous"],char:'😧',fitzpatrick_scale:false,category:"people"},cry:{keywords:["face","tears","sad","depressed","upset",":'("],char:'😢',fitzpatrick_scale:false,category:"people"},disappointed_relieved:{keywords:["face","phew","sweat","nervous"],char:'😥',fitzpatrick_scale:false,category:"people"},drooling_face:{keywords:["face"],char:'🤤',fitzpatrick_scale:false,category:"people"},sleepy:{keywords:["face","tired","rest","nap"],char:'😪',fitzpatrick_scale:false,category:"people"},sweat:{keywords:["face","hot","sad","tired","exercise"],char:'😓',fitzpatrick_scale:false,category:"people"},hot:{keywords:["face","feverish","heat","red","sweating"],char:'🥵',fitzpatrick_scale:false,category:"people"},cold:{keywords:["face","blue","freezing","frozen","frostbite","icicles"],char:'🥶',fitzpatrick_scale:false,category:"people"},sob:{keywords:["face","cry","tears","sad","upset","depressed"],char:'😭',fitzpatrick_scale:false,category:"people"},dizzy_face:{keywords:["spent","unconscious","xox","dizzy"],char:'😵',fitzpatrick_scale:false,category:"people"},astonished:{keywords:["face","xox","surprised","poisoned"],char:'😲',fitzpatrick_scale:false,category:"people"},zipper_mouth_face:{keywords:["face","sealed","zipper","secret"],char:'🤐',fitzpatrick_scale:false,category:"people"},nauseated_face:{keywords:["face","vomit","gross","green","sick","throw up","ill"],char:'🤢',fitzpatrick_scale:false,category:"people"},sneezing_face:{keywords:["face","gesundheit","sneeze","sick","allergy"],char:'🤧',fitzpatrick_scale:false,category:"people"},vomiting:{keywords:["face","sick"],char:'🤮',fitzpatrick_scale:false,category:"people"},mask:{keywords:["face","sick","ill","disease"],char:'😷',fitzpatrick_scale:false,category:"people"},face_with_thermometer:{keywords:["sick","temperature","thermometer","cold","fever"],char:'🤒',fitzpatrick_scale:false,category:"people"},face_with_head_bandage:{keywords:["injured","clumsy","bandage","hurt"],char:'🤕',fitzpatrick_scale:false,category:"people"},woozy:{keywords:["face","dizzy","intoxicated","tipsy","wavy"],char:'🥴',fitzpatrick_scale:false,category:"people"},sleeping:{keywords:["face","tired","sleepy","night","zzz"],char:'😴',fitzpatrick_scale:false,category:"people"},zzz:{keywords:["sleepy","tired","dream"],char:'💤',fitzpatrick_scale:false,category:"people"},poop:{keywords:["hankey","shitface","fail","turd","shit"],char:'💩',fitzpatrick_scale:false,category:"people"},smiling_imp:{keywords:["devil","horns"],char:'😈',fitzpatrick_scale:false,category:"people"},imp:{keywords:["devil","angry","horns"],char:'👿',fitzpatrick_scale:false,category:"people"},japanese_ogre:{keywords:["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"],char:'👹',fitzpatrick_scale:false,category:"people"},japanese_goblin:{keywords:["red","evil","mask","monster","scary","creepy","japanese","goblin"],char:'👺',fitzpatrick_scale:false,category:"people"},skull:{keywords:["dead","skeleton","creepy","death"],char:'💀',fitzpatrick_scale:false,category:"people"},ghost:{keywords:["halloween","spooky","scary"],char:'👻',fitzpatrick_scale:false,category:"people"},alien:{keywords:["UFO","paul","weird","outer_space"],char:'👽',fitzpatrick_scale:false,category:"people"},robot:{keywords:["computer","machine","bot"],char:'🤖',fitzpatrick_scale:false,category:"people"},smiley_cat:{keywords:["animal","cats","happy","smile"],char:'😺',fitzpatrick_scale:false,category:"people"},smile_cat:{keywords:["animal","cats","smile"],char:'😸',fitzpatrick_scale:false,category:"people"},joy_cat:{keywords:["animal","cats","haha","happy","tears"],char:'😹',fitzpatrick_scale:false,category:"people"},heart_eyes_cat:{keywords:["animal","love","like","affection","cats","valentines","heart"],char:'😻',fitzpatrick_scale:false,category:"people"},smirk_cat:{keywords:["animal","cats","smirk"],char:'😼',fitzpatrick_scale:false,category:"people"},kissing_cat:{keywords:["animal","cats","kiss"],char:'😽',fitzpatrick_scale:false,category:"people"},scream_cat:{keywords:["animal","cats","munch","scared","scream"],char:'🙀',fitzpatrick_scale:false,category:"people"},crying_cat_face:{keywords:["animal","tears","weep","sad","cats","upset","cry"],char:'😿',fitzpatrick_scale:false,category:"people"},pouting_cat:{keywords:["animal","cats"],char:'😾',fitzpatrick_scale:false,category:"people"},palms_up:{keywords:["hands","gesture","cupped","prayer"],char:'🤲',fitzpatrick_scale:true,category:"people"},raised_hands:{keywords:["gesture","hooray","yea","celebration","hands"],char:'🙌',fitzpatrick_scale:true,category:"people"},clap:{keywords:["hands","praise","applause","congrats","yay"],char:'👏',fitzpatrick_scale:true,category:"people"},wave:{keywords:["hands","gesture","goodbye","solong","farewell","hello","hi","palm"],char:'👋',fitzpatrick_scale:true,category:"people"},call_me_hand:{keywords:["hands","gesture"],char:'🤙',fitzpatrick_scale:true,category:"people"},"+1":{keywords:["thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],char:'👍',fitzpatrick_scale:true,category:"people"},"-1":{keywords:["thumbsdown","no","dislike","hand"],char:'👎',fitzpatrick_scale:true,category:"people"},facepunch:{keywords:["angry","violence","fist","hit","attack","hand"],char:'👊',fitzpatrick_scale:true,category:"people"},fist:{keywords:["fingers","hand","grasp"],char:'✊',fitzpatrick_scale:true,category:"people"},fist_left:{keywords:["hand","fistbump"],char:'🤛',fitzpatrick_scale:true,category:"people"},fist_right:{keywords:["hand","fistbump"],char:'🤜',fitzpatrick_scale:true,category:"people"},v:{keywords:["fingers","ohyeah","hand","peace","victory","two"],char:'✌',fitzpatrick_scale:true,category:"people"},ok_hand:{keywords:["fingers","limbs","perfect","ok","okay"],char:'👌',fitzpatrick_scale:true,category:"people"},raised_hand:{keywords:["fingers","stop","highfive","palm","ban"],char:'✋',fitzpatrick_scale:true,category:"people"},raised_back_of_hand:{keywords:["fingers","raised","backhand"],char:'🤚',fitzpatrick_scale:true,category:"people"},open_hands:{keywords:["fingers","butterfly","hands","open"],char:'👐',fitzpatrick_scale:true,category:"people"},muscle:{keywords:["arm","flex","hand","summer","strong","biceps"],char:'💪',fitzpatrick_scale:true,category:"people"},pray:{keywords:["please","hope","wish","namaste","highfive"],char:'🙏',fitzpatrick_scale:true,category:"people"},foot:{keywords:["kick","stomp"],char:'🦶',fitzpatrick_scale:true,category:"people"},leg:{keywords:["kick","limb"],char:'🦵',fitzpatrick_scale:true,category:"people"},handshake:{keywords:["agreement","shake"],char:'🤝',fitzpatrick_scale:false,category:"people"},point_up:{keywords:["hand","fingers","direction","up"],char:'☝',fitzpatrick_scale:true,category:"people"},point_up_2:{keywords:["fingers","hand","direction","up"],char:'👆',fitzpatrick_scale:true,category:"people"},point_down:{keywords:["fingers","hand","direction","down"],char:'👇',fitzpatrick_scale:true,category:"people"},point_left:{keywords:["direction","fingers","hand","left"],char:'👈',fitzpatrick_scale:true,category:"people"},point_right:{keywords:["fingers","hand","direction","right"],char:'👉',fitzpatrick_scale:true,category:"people"},fu:{keywords:["hand","fingers","rude","middle","flipping"],char:'🖕',fitzpatrick_scale:true,category:"people"},raised_hand_with_fingers_splayed:{keywords:["hand","fingers","palm"],char:'🖐',fitzpatrick_scale:true,category:"people"},love_you:{keywords:["hand","fingers","gesture"],char:'🤟',fitzpatrick_scale:true,category:"people"},metal:{keywords:["hand","fingers","evil_eye","sign_of_horns","rock_on"],char:'🤘',fitzpatrick_scale:true,category:"people"},crossed_fingers:{keywords:["good","lucky"],char:'🤞',fitzpatrick_scale:true,category:"people"},vulcan_salute:{keywords:["hand","fingers","spock","star trek"],char:'🖖',fitzpatrick_scale:true,category:"people"},writing_hand:{keywords:["lower_left_ballpoint_pen","stationery","write","compose"],char:'✍',fitzpatrick_scale:true,category:"people"},selfie:{keywords:["camera","phone"],char:'🤳',fitzpatrick_scale:true,category:"people"},nail_care:{keywords:["beauty","manicure","finger","fashion","nail"],char:'💅',fitzpatrick_scale:true,category:"people"},lips:{keywords:["mouth","kiss"],char:'👄',fitzpatrick_scale:false,category:"people"},tooth:{keywords:["teeth","dentist"],char:'🦷',fitzpatrick_scale:false,category:"people"},tongue:{keywords:["mouth","playful"],char:'👅',fitzpatrick_scale:false,category:"people"},ear:{keywords:["face","hear","sound","listen"],char:'👂',fitzpatrick_scale:true,category:"people"},nose:{keywords:["smell","sniff"],char:'👃',fitzpatrick_scale:true,category:"people"},eye:{keywords:["face","look","see","watch","stare"],char:'👁',fitzpatrick_scale:false,category:"people"},eyes:{keywords:["look","watch","stalk","peek","see"],char:'👀',fitzpatrick_scale:false,category:"people"},brain:{keywords:["smart","intelligent"],char:'🧠',fitzpatrick_scale:false,category:"people"},bust_in_silhouette:{keywords:["user","person","human"],char:'👤',fitzpatrick_scale:false,category:"people"},busts_in_silhouette:{keywords:["user","person","human","group","team"],char:'👥',fitzpatrick_scale:false,category:"people"},speaking_head:{keywords:["user","person","human","sing","say","talk"],char:'🗣',fitzpatrick_scale:false,category:"people"},baby:{keywords:["child","boy","girl","toddler"],char:'👶',fitzpatrick_scale:true,category:"people"},child:{keywords:["gender-neutral","young"],char:'🧒',fitzpatrick_scale:true,category:"people"},boy:{keywords:["man","male","guy","teenager"],char:'👦',fitzpatrick_scale:true,category:"people"},girl:{keywords:["female","woman","teenager"],char:'👧',fitzpatrick_scale:true,category:"people"},adult:{keywords:["gender-neutral","person"],char:'🧑',fitzpatrick_scale:true,category:"people"},man:{keywords:["mustache","father","dad","guy","classy","sir","moustache"],char:'👨',fitzpatrick_scale:true,category:"people"},woman:{keywords:["female","girls","lady"],char:'👩',fitzpatrick_scale:true,category:"people"},blonde_woman:{keywords:["woman","female","girl","blonde","person"],char:'👱‍♀️',fitzpatrick_scale:true,category:"people"},blonde_man:{keywords:["man","male","boy","blonde","guy","person"],char:'👱',fitzpatrick_scale:true,category:"people"},bearded_person:{keywords:["person","bewhiskered"],char:'🧔',fitzpatrick_scale:true,category:"people"},older_adult:{keywords:["human","elder","senior","gender-neutral"],char:'🧓',fitzpatrick_scale:true,category:"people"},older_man:{keywords:["human","male","men","old","elder","senior"],char:'👴',fitzpatrick_scale:true,category:"people"},older_woman:{keywords:["human","female","women","lady","old","elder","senior"],char:'👵',fitzpatrick_scale:true,category:"people"},man_with_gua_pi_mao:{keywords:["male","boy","chinese"],char:'👲',fitzpatrick_scale:true,category:"people"},woman_with_headscarf:{keywords:["female","hijab","mantilla","tichel"],char:'🧕',fitzpatrick_scale:true,category:"people"},woman_with_turban:{keywords:["female","indian","hinduism","arabs","woman"],char:'👳‍♀️',fitzpatrick_scale:true,category:"people"},man_with_turban:{keywords:["male","indian","hinduism","arabs"],char:'👳',fitzpatrick_scale:true,category:"people"},policewoman:{keywords:["woman","police","law","legal","enforcement","arrest","911","female"],char:'👮‍♀️',fitzpatrick_scale:true,category:"people"},policeman:{keywords:["man","police","law","legal","enforcement","arrest","911"],char:'👮',fitzpatrick_scale:true,category:"people"},construction_worker_woman:{keywords:["female","human","wip","build","construction","worker","labor","woman"],char:'👷‍♀️',fitzpatrick_scale:true,category:"people"},construction_worker_man:{keywords:["male","human","wip","guy","build","construction","worker","labor"],char:'👷',fitzpatrick_scale:true,category:"people"},guardswoman:{keywords:["uk","gb","british","female","royal","woman"],char:'💂‍♀️',fitzpatrick_scale:true,category:"people"},guardsman:{keywords:["uk","gb","british","male","guy","royal"],char:'💂',fitzpatrick_scale:true,category:"people"},female_detective:{keywords:["human","spy","detective","female","woman"],char:'🕵️‍♀️',fitzpatrick_scale:true,category:"people"},male_detective:{keywords:["human","spy","detective"],char:'🕵',fitzpatrick_scale:true,category:"people"},woman_health_worker:{keywords:["doctor","nurse","therapist","healthcare","woman","human"],char:'👩‍⚕️',fitzpatrick_scale:true,category:"people"},man_health_worker:{keywords:["doctor","nurse","therapist","healthcare","man","human"],char:'👨‍⚕️',fitzpatrick_scale:true,category:"people"},woman_farmer:{keywords:["rancher","gardener","woman","human"],char:'👩‍🌾',fitzpatrick_scale:true,category:"people"},man_farmer:{keywords:["rancher","gardener","man","human"],char:'👨‍🌾',fitzpatrick_scale:true,category:"people"},woman_cook:{keywords:["chef","woman","human"],char:'👩‍🍳',fitzpatrick_scale:true,category:"people"},man_cook:{keywords:["chef","man","human"],char:'👨‍🍳',fitzpatrick_scale:true,category:"people"},woman_student:{keywords:["graduate","woman","human"],char:'👩‍🎓',fitzpatrick_scale:true,category:"people"},man_student:{keywords:["graduate","man","human"],char:'👨‍🎓',fitzpatrick_scale:true,category:"people"},woman_singer:{keywords:["rockstar","entertainer","woman","human"],char:'👩‍🎤',fitzpatrick_scale:true,category:"people"},man_singer:{keywords:["rockstar","entertainer","man","human"],char:'👨‍🎤',fitzpatrick_scale:true,category:"people"},woman_teacher:{keywords:["instructor","professor","woman","human"],char:'👩‍🏫',fitzpatrick_scale:true,category:"people"},man_teacher:{keywords:["instructor","professor","man","human"],char:'👨‍🏫',fitzpatrick_scale:true,category:"people"},woman_factory_worker:{keywords:["assembly","industrial","woman","human"],char:'👩‍🏭',fitzpatrick_scale:true,category:"people"},man_factory_worker:{keywords:["assembly","industrial","man","human"],char:'👨‍🏭',fitzpatrick_scale:true,category:"people"},woman_technologist:{keywords:["coder","developer","engineer","programmer","software","woman","human","laptop","computer"],char:'👩‍💻',fitzpatrick_scale:true,category:"people"},man_technologist:{keywords:["coder","developer","engineer","programmer","software","man","human","laptop","computer"],char:'👨‍💻',fitzpatrick_scale:true,category:"people"},woman_office_worker:{keywords:["business","manager","woman","human"],char:'👩‍💼',fitzpatrick_scale:true,category:"people"},man_office_worker:{keywords:["business","manager","man","human"],char:'👨‍💼',fitzpatrick_scale:true,category:"people"},woman_mechanic:{keywords:["plumber","woman","human","wrench"],char:'👩‍🔧',fitzpatrick_scale:true,category:"people"},man_mechanic:{keywords:["plumber","man","human","wrench"],char:'👨‍🔧',fitzpatrick_scale:true,category:"people"},woman_scientist:{keywords:["biologist","chemist","engineer","physicist","woman","human"],char:'👩‍🔬',fitzpatrick_scale:true,category:"people"},man_scientist:{keywords:["biologist","chemist","engineer","physicist","man","human"],char:'👨‍🔬',fitzpatrick_scale:true,category:"people"},woman_artist:{keywords:["painter","woman","human"],char:'👩‍🎨',fitzpatrick_scale:true,category:"people"},man_artist:{keywords:["painter","man","human"],char:'👨‍🎨',fitzpatrick_scale:true,category:"people"},woman_firefighter:{keywords:["fireman","woman","human"],char:'👩‍🚒',fitzpatrick_scale:true,category:"people"},man_firefighter:{keywords:["fireman","man","human"],char:'👨‍🚒',fitzpatrick_scale:true,category:"people"},woman_pilot:{keywords:["aviator","plane","woman","human"],char:'👩‍✈️',fitzpatrick_scale:true,category:"people"},man_pilot:{keywords:["aviator","plane","man","human"],char:'👨‍✈️',fitzpatrick_scale:true,category:"people"},woman_astronaut:{keywords:["space","rocket","woman","human"],char:'👩‍🚀',fitzpatrick_scale:true,category:"people"},man_astronaut:{keywords:["space","rocket","man","human"],char:'👨‍🚀',fitzpatrick_scale:true,category:"people"},woman_judge:{keywords:["justice","court","woman","human"],char:'👩‍⚖️',fitzpatrick_scale:true,category:"people"},man_judge:{keywords:["justice","court","man","human"],char:'👨‍⚖️',fitzpatrick_scale:true,category:"people"},woman_superhero:{keywords:["woman","female","good","heroine","superpowers"],char:'🦸‍♀️',fitzpatrick_scale:true,category:"people"},man_superhero:{keywords:["man","male","good","hero","superpowers"],char:'🦸‍♂️',fitzpatrick_scale:true,category:"people"},woman_supervillain:{keywords:["woman","female","evil","bad","criminal","heroine","superpowers"],char:'🦹‍♀️',fitzpatrick_scale:true,category:"people"},man_supervillain:{keywords:["man","male","evil","bad","criminal","hero","superpowers"],char:'🦹‍♂️',fitzpatrick_scale:true,category:"people"},mrs_claus:{keywords:["woman","female","xmas","mother christmas"],char:'🤶',fitzpatrick_scale:true,category:"people"},santa:{keywords:["festival","man","male","xmas","father christmas"],char:'🎅',fitzpatrick_scale:true,category:"people"},sorceress:{keywords:["woman","female","mage","witch"],char:'🧙‍♀️',fitzpatrick_scale:true,category:"people"},wizard:{keywords:["man","male","mage","sorcerer"],char:'🧙‍♂️',fitzpatrick_scale:true,category:"people"},woman_elf:{keywords:["woman","female"],char:'🧝‍♀️',fitzpatrick_scale:true,category:"people"},man_elf:{keywords:["man","male"],char:'🧝‍♂️',fitzpatrick_scale:true,category:"people"},woman_vampire:{keywords:["woman","female"],char:'🧛‍♀️',fitzpatrick_scale:true,category:"people"},man_vampire:{keywords:["man","male","dracula"],char:'🧛‍♂️',fitzpatrick_scale:true,category:"people"},woman_zombie:{keywords:["woman","female","undead","walking dead"],char:'🧟‍♀️',fitzpatrick_scale:false,category:"people"},man_zombie:{keywords:["man","male","dracula","undead","walking dead"],char:'🧟‍♂️',fitzpatrick_scale:false,category:"people"},woman_genie:{keywords:["woman","female"],char:'🧞‍♀️',fitzpatrick_scale:false,category:"people"},man_genie:{keywords:["man","male"],char:'🧞‍♂️',fitzpatrick_scale:false,category:"people"},mermaid:{keywords:["woman","female","merwoman","ariel"],char:'🧜‍♀️',fitzpatrick_scale:true,category:"people"},merman:{keywords:["man","male","triton"],char:'🧜‍♂️',fitzpatrick_scale:true,category:"people"},woman_fairy:{keywords:["woman","female"],char:'🧚‍♀️',fitzpatrick_scale:true,category:"people"},man_fairy:{keywords:["man","male"],char:'🧚‍♂️',fitzpatrick_scale:true,category:"people"},angel:{keywords:["heaven","wings","halo"],char:'👼',fitzpatrick_scale:true,category:"people"},pregnant_woman:{keywords:["baby"],char:'🤰',fitzpatrick_scale:true,category:"people"},breastfeeding:{keywords:["nursing","baby"],char:'🤱',fitzpatrick_scale:true,category:"people"},princess:{keywords:["girl","woman","female","blond","crown","royal","queen"],char:'👸',fitzpatrick_scale:true,category:"people"},prince:{keywords:["boy","man","male","crown","royal","king"],char:'🤴',fitzpatrick_scale:true,category:"people"},bride_with_veil:{keywords:["couple","marriage","wedding","woman","bride"],char:'👰',fitzpatrick_scale:true,category:"people"},man_in_tuxedo:{keywords:["couple","marriage","wedding","groom"],char:'🤵',fitzpatrick_scale:true,category:"people"},running_woman:{keywords:["woman","walking","exercise","race","running","female"],char:'🏃‍♀️',fitzpatrick_scale:true,category:"people"},running_man:{keywords:["man","walking","exercise","race","running"],char:'🏃',fitzpatrick_scale:true,category:"people"},walking_woman:{keywords:["human","feet","steps","woman","female"],char:'🚶‍♀️',fitzpatrick_scale:true,category:"people"},walking_man:{keywords:["human","feet","steps"],char:'🚶',fitzpatrick_scale:true,category:"people"},dancer:{keywords:["female","girl","woman","fun"],char:'💃',fitzpatrick_scale:true,category:"people"},man_dancing:{keywords:["male","boy","fun","dancer"],char:'🕺',fitzpatrick_scale:true,category:"people"},dancing_women:{keywords:["female","bunny","women","girls"],char:'👯',fitzpatrick_scale:false,category:"people"},dancing_men:{keywords:["male","bunny","men","boys"],char:'👯‍♂️',fitzpatrick_scale:false,category:"people"},couple:{keywords:["pair","people","human","love","date","dating","like","affection","valentines","marriage"],char:'👫',fitzpatrick_scale:false,category:"people"},two_men_holding_hands:{keywords:["pair","couple","love","like","bromance","friendship","people","human"],char:'👬',fitzpatrick_scale:false,category:"people"},two_women_holding_hands:{keywords:["pair","friendship","couple","love","like","female","people","human"],char:'👭',fitzpatrick_scale:false,category:"people"},bowing_woman:{keywords:["woman","female","girl"],char:'🙇‍♀️',fitzpatrick_scale:true,category:"people"},bowing_man:{keywords:["man","male","boy"],char:'🙇',fitzpatrick_scale:true,category:"people"},man_facepalming:{keywords:["man","male","boy","disbelief"],char:'🤦‍♂️',fitzpatrick_scale:true,category:"people"},woman_facepalming:{keywords:["woman","female","girl","disbelief"],char:'🤦‍♀️',fitzpatrick_scale:true,category:"people"},woman_shrugging:{keywords:["woman","female","girl","confused","indifferent","doubt"],char:'🤷',fitzpatrick_scale:true,category:"people"},man_shrugging:{keywords:["man","male","boy","confused","indifferent","doubt"],char:'🤷‍♂️',fitzpatrick_scale:true,category:"people"},tipping_hand_woman:{keywords:["female","girl","woman","human","information"],char:'💁',fitzpatrick_scale:true,category:"people"},tipping_hand_man:{keywords:["male","boy","man","human","information"],char:'💁‍♂️',fitzpatrick_scale:true,category:"people"},no_good_woman:{keywords:["female","girl","woman","nope"],char:'🙅',fitzpatrick_scale:true,category:"people"},no_good_man:{keywords:["male","boy","man","nope"],char:'🙅‍♂️',fitzpatrick_scale:true,category:"people"},ok_woman:{keywords:["women","girl","female","pink","human","woman"],char:'🙆',fitzpatrick_scale:true,category:"people"},ok_man:{keywords:["men","boy","male","blue","human","man"],char:'🙆‍♂️',fitzpatrick_scale:true,category:"people"},raising_hand_woman:{keywords:["female","girl","woman"],char:'🙋',fitzpatrick_scale:true,category:"people"},raising_hand_man:{keywords:["male","boy","man"],char:'🙋‍♂️',fitzpatrick_scale:true,category:"people"},pouting_woman:{keywords:["female","girl","woman"],char:'🙎',fitzpatrick_scale:true,category:"people"},pouting_man:{keywords:["male","boy","man"],char:'🙎‍♂️',fitzpatrick_scale:true,category:"people"},frowning_woman:{keywords:["female","girl","woman","sad","depressed","discouraged","unhappy"],char:'🙍',fitzpatrick_scale:true,category:"people"},frowning_man:{keywords:["male","boy","man","sad","depressed","discouraged","unhappy"],char:'🙍‍♂️',fitzpatrick_scale:true,category:"people"},haircut_woman:{keywords:["female","girl","woman"],char:'💇',fitzpatrick_scale:true,category:"people"},haircut_man:{keywords:["male","boy","man"],char:'💇‍♂️',fitzpatrick_scale:true,category:"people"},massage_woman:{keywords:["female","girl","woman","head"],char:'💆',fitzpatrick_scale:true,category:"people"},massage_man:{keywords:["male","boy","man","head"],char:'💆‍♂️',fitzpatrick_scale:true,category:"people"},woman_in_steamy_room:{keywords:["female","woman","spa","steamroom","sauna"],char:'🧖‍♀️',fitzpatrick_scale:true,category:"people"},man_in_steamy_room:{keywords:["male","man","spa","steamroom","sauna"],char:'🧖‍♂️',fitzpatrick_scale:true,category:"people"},couple_with_heart_woman_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'💑',fitzpatrick_scale:false,category:"people"},couple_with_heart_woman_woman:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'👩‍❤️‍👩',fitzpatrick_scale:false,category:"people"},couple_with_heart_man_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'👨‍❤️‍👨',fitzpatrick_scale:false,category:"people"},couplekiss_man_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:'💏',fitzpatrick_scale:false,category:"people"},couplekiss_woman_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:'👩‍❤️‍💋‍👩',fitzpatrick_scale:false,category:"people"},couplekiss_man_man:{keywords:["pair","valentines","love","like","dating","marriage"],char:'👨‍❤️‍💋‍👨',fitzpatrick_scale:false,category:"people"},family_man_woman_boy:{keywords:["home","parents","child","mom","dad","father","mother","people","human"],char:'👪',fitzpatrick_scale:false,category:"people"},family_man_woman_girl:{keywords:["home","parents","people","human","child"],char:'👨‍👩‍👧',fitzpatrick_scale:false,category:"people"},family_man_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👩‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_man_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👩‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_man_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:'👨‍👩‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_woman_woman_boy:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👦',fitzpatrick_scale:false,category:"people"},family_woman_woman_girl:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👧',fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_woman_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:'👩‍👩‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_man_man_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👦',fitzpatrick_scale:false,category:"people"},family_man_man_girl:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👧',fitzpatrick_scale:false,category:"people"},family_man_man_girl_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_man_man_boy_boy:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_man_man_girl_girl:{keywords:["home","parents","people","human","children"],char:'👨‍👨‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_woman_boy:{keywords:["home","parent","people","human","child"],char:'👩‍👦',fitzpatrick_scale:false,category:"people"},family_woman_girl:{keywords:["home","parent","people","human","child"],char:'👩‍👧',fitzpatrick_scale:false,category:"people"},family_woman_girl_boy:{keywords:["home","parent","people","human","children"],char:'👩‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_woman_boy_boy:{keywords:["home","parent","people","human","children"],char:'👩‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_woman_girl_girl:{keywords:["home","parent","people","human","children"],char:'👩‍👧‍👧',fitzpatrick_scale:false,category:"people"},family_man_boy:{keywords:["home","parent","people","human","child"],char:'👨‍👦',fitzpatrick_scale:false,category:"people"},family_man_girl:{keywords:["home","parent","people","human","child"],char:'👨‍👧',fitzpatrick_scale:false,category:"people"},family_man_girl_boy:{keywords:["home","parent","people","human","children"],char:'👨‍👧‍👦',fitzpatrick_scale:false,category:"people"},family_man_boy_boy:{keywords:["home","parent","people","human","children"],char:'👨‍👦‍👦',fitzpatrick_scale:false,category:"people"},family_man_girl_girl:{keywords:["home","parent","people","human","children"],char:'👨‍👧‍👧',fitzpatrick_scale:false,category:"people"},yarn:{keywords:["ball","crochet","knit"],char:'🧶',fitzpatrick_scale:false,category:"people"},thread:{keywords:["needle","sewing","spool","string"],char:'🧵',fitzpatrick_scale:false,category:"people"},coat:{keywords:["jacket"],char:'🧥',fitzpatrick_scale:false,category:"people"},labcoat:{keywords:["doctor","experiment","scientist","chemist"],char:'🥼',fitzpatrick_scale:false,category:"people"},womans_clothes:{keywords:["fashion","shopping_bags","female"],char:'👚',fitzpatrick_scale:false,category:"people"},tshirt:{keywords:["fashion","cloth","casual","shirt","tee"],char:'👕',fitzpatrick_scale:false,category:"people"},jeans:{keywords:["fashion","shopping"],char:'👖',fitzpatrick_scale:false,category:"people"},necktie:{keywords:["shirt","suitup","formal","fashion","cloth","business"],char:'👔',fitzpatrick_scale:false,category:"people"},dress:{keywords:["clothes","fashion","shopping"],char:'👗',fitzpatrick_scale:false,category:"people"},bikini:{keywords:["swimming","female","woman","girl","fashion","beach","summer"],char:'👙',fitzpatrick_scale:false,category:"people"},kimono:{keywords:["dress","fashion","women","female","japanese"],char:'👘',fitzpatrick_scale:false,category:"people"},lipstick:{keywords:["female","girl","fashion","woman"],char:'💄',fitzpatrick_scale:false,category:"people"},kiss:{keywords:["face","lips","love","like","affection","valentines"],char:'💋',fitzpatrick_scale:false,category:"people"},footprints:{keywords:["feet","tracking","walking","beach"],char:'👣',fitzpatrick_scale:false,category:"people"},flat_shoe:{keywords:["ballet","slip-on","slipper"],char:'🥿',fitzpatrick_scale:false,category:"people"},high_heel:{keywords:["fashion","shoes","female","pumps","stiletto"],char:'👠',fitzpatrick_scale:false,category:"people"},sandal:{keywords:["shoes","fashion","flip flops"],char:'👡',fitzpatrick_scale:false,category:"people"},boot:{keywords:["shoes","fashion"],char:'👢',fitzpatrick_scale:false,category:"people"},mans_shoe:{keywords:["fashion","male"],char:'👞',fitzpatrick_scale:false,category:"people"},athletic_shoe:{keywords:["shoes","sports","sneakers"],char:'👟',fitzpatrick_scale:false,category:"people"},hiking_boot:{keywords:["backpacking","camping","hiking"],char:'🥾',fitzpatrick_scale:false,category:"people"},socks:{keywords:["stockings","clothes"],char:'🧦',fitzpatrick_scale:false,category:"people"},gloves:{keywords:["hands","winter","clothes"],char:'🧤',fitzpatrick_scale:false,category:"people"},scarf:{keywords:["neck","winter","clothes"],char:'🧣',fitzpatrick_scale:false,category:"people"},womans_hat:{keywords:["fashion","accessories","female","lady","spring"],char:'👒',fitzpatrick_scale:false,category:"people"},tophat:{keywords:["magic","gentleman","classy","circus"],char:'🎩',fitzpatrick_scale:false,category:"people"},billed_hat:{keywords:["cap","baseball"],char:'🧢',fitzpatrick_scale:false,category:"people"},rescue_worker_helmet:{keywords:["construction","build"],char:'⛑',fitzpatrick_scale:false,category:"people"},mortar_board:{keywords:["school","college","degree","university","graduation","cap","hat","legal","learn","education"],char:'🎓',fitzpatrick_scale:false,category:"people"},crown:{keywords:["king","kod","leader","royalty","lord"],char:'👑',fitzpatrick_scale:false,category:"people"},school_satchel:{keywords:["student","education","bag","backpack"],char:'🎒',fitzpatrick_scale:false,category:"people"},luggage:{keywords:["packing","travel"],char:'🧳',fitzpatrick_scale:false,category:"people"},pouch:{keywords:["bag","accessories","shopping"],char:'👝',fitzpatrick_scale:false,category:"people"},purse:{keywords:["fashion","accessories","money","sales","shopping"],char:'👛',fitzpatrick_scale:false,category:"people"},handbag:{keywords:["fashion","accessory","accessories","shopping"],char:'👜',fitzpatrick_scale:false,category:"people"},briefcase:{keywords:["business","documents","work","law","legal","job","career"],char:'💼',fitzpatrick_scale:false,category:"people"},eyeglasses:{keywords:["fashion","accessories","eyesight","nerdy","dork","geek"],char:'👓',fitzpatrick_scale:false,category:"people"},dark_sunglasses:{keywords:["face","cool","accessories"],char:'🕶',fitzpatrick_scale:false,category:"people"},goggles:{keywords:["eyes","protection","safety"],char:'🥽',fitzpatrick_scale:false,category:"people"},ring:{keywords:["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"],char:'💍',fitzpatrick_scale:false,category:"people"},closed_umbrella:{keywords:["weather","rain","drizzle"],char:'🌂',fitzpatrick_scale:false,category:"people"},dog:{keywords:["animal","friend","nature","woof","puppy","pet","faithful"],char:'🐶',fitzpatrick_scale:false,category:"animals_and_nature"},cat:{keywords:["animal","meow","nature","pet","kitten"],char:'🐱',fitzpatrick_scale:false,category:"animals_and_nature"},mouse:{keywords:["animal","nature","cheese_wedge","rodent"],char:'🐭',fitzpatrick_scale:false,category:"animals_and_nature"},hamster:{keywords:["animal","nature"],char:'🐹',fitzpatrick_scale:false,category:"animals_and_nature"},rabbit:{keywords:["animal","nature","pet","spring","magic","bunny"],char:'🐰',fitzpatrick_scale:false,category:"animals_and_nature"},fox_face:{keywords:["animal","nature","face"],char:'🦊',fitzpatrick_scale:false,category:"animals_and_nature"},bear:{keywords:["animal","nature","wild"],char:'🐻',fitzpatrick_scale:false,category:"animals_and_nature"},panda_face:{keywords:["animal","nature","panda"],char:'🐼',fitzpatrick_scale:false,category:"animals_and_nature"},koala:{keywords:["animal","nature"],char:'🐨',fitzpatrick_scale:false,category:"animals_and_nature"},tiger:{keywords:["animal","cat","danger","wild","nature","roar"],char:'🐯',fitzpatrick_scale:false,category:"animals_and_nature"},lion:{keywords:["animal","nature"],char:'🦁',fitzpatrick_scale:false,category:"animals_and_nature"},cow:{keywords:["beef","ox","animal","nature","moo","milk"],char:'🐮',fitzpatrick_scale:false,category:"animals_and_nature"},pig:{keywords:["animal","oink","nature"],char:'🐷',fitzpatrick_scale:false,category:"animals_and_nature"},pig_nose:{keywords:["animal","oink"],char:'🐽',fitzpatrick_scale:false,category:"animals_and_nature"},frog:{keywords:["animal","nature","croak","toad"],char:'🐸',fitzpatrick_scale:false,category:"animals_and_nature"},squid:{keywords:["animal","nature","ocean","sea"],char:'🦑',fitzpatrick_scale:false,category:"animals_and_nature"},octopus:{keywords:["animal","creature","ocean","sea","nature","beach"],char:'🐙',fitzpatrick_scale:false,category:"animals_and_nature"},shrimp:{keywords:["animal","ocean","nature","seafood"],char:'🦐',fitzpatrick_scale:false,category:"animals_and_nature"},monkey_face:{keywords:["animal","nature","circus"],char:'🐵',fitzpatrick_scale:false,category:"animals_and_nature"},gorilla:{keywords:["animal","nature","circus"],char:'🦍',fitzpatrick_scale:false,category:"animals_and_nature"},see_no_evil:{keywords:["monkey","animal","nature","haha"],char:'🙈',fitzpatrick_scale:false,category:"animals_and_nature"},hear_no_evil:{keywords:["animal","monkey","nature"],char:'🙉',fitzpatrick_scale:false,category:"animals_and_nature"},speak_no_evil:{keywords:["monkey","animal","nature","omg"],char:'🙊',fitzpatrick_scale:false,category:"animals_and_nature"},monkey:{keywords:["animal","nature","banana","circus"],char:'🐒',fitzpatrick_scale:false,category:"animals_and_nature"},chicken:{keywords:["animal","cluck","nature","bird"],char:'🐔',fitzpatrick_scale:false,category:"animals_and_nature"},penguin:{keywords:["animal","nature"],char:'🐧',fitzpatrick_scale:false,category:"animals_and_nature"},bird:{keywords:["animal","nature","fly","tweet","spring"],char:'🐦',fitzpatrick_scale:false,category:"animals_and_nature"},baby_chick:{keywords:["animal","chicken","bird"],char:'🐤',fitzpatrick_scale:false,category:"animals_and_nature"},hatching_chick:{keywords:["animal","chicken","egg","born","baby","bird"],char:'🐣',fitzpatrick_scale:false,category:"animals_and_nature"},hatched_chick:{keywords:["animal","chicken","baby","bird"],char:'🐥',fitzpatrick_scale:false,category:"animals_and_nature"},duck:{keywords:["animal","nature","bird","mallard"],char:'🦆',fitzpatrick_scale:false,category:"animals_and_nature"},eagle:{keywords:["animal","nature","bird"],char:'🦅',fitzpatrick_scale:false,category:"animals_and_nature"},owl:{keywords:["animal","nature","bird","hoot"],char:'🦉',fitzpatrick_scale:false,category:"animals_and_nature"},bat:{keywords:["animal","nature","blind","vampire"],char:'🦇',fitzpatrick_scale:false,category:"animals_and_nature"},wolf:{keywords:["animal","nature","wild"],char:'🐺',fitzpatrick_scale:false,category:"animals_and_nature"},boar:{keywords:["animal","nature"],char:'🐗',fitzpatrick_scale:false,category:"animals_and_nature"},horse:{keywords:["animal","brown","nature"],char:'🐴',fitzpatrick_scale:false,category:"animals_and_nature"},unicorn:{keywords:["animal","nature","mystical"],char:'🦄',fitzpatrick_scale:false,category:"animals_and_nature"},honeybee:{keywords:["animal","insect","nature","bug","spring","honey"],char:'🐝',fitzpatrick_scale:false,category:"animals_and_nature"},bug:{keywords:["animal","insect","nature","worm"],char:'🐛',fitzpatrick_scale:false,category:"animals_and_nature"},butterfly:{keywords:["animal","insect","nature","caterpillar"],char:'🦋',fitzpatrick_scale:false,category:"animals_and_nature"},snail:{keywords:["slow","animal","shell"],char:'🐌',fitzpatrick_scale:false,category:"animals_and_nature"},beetle:{keywords:["animal","insect","nature","ladybug"],char:'🐞',fitzpatrick_scale:false,category:"animals_and_nature"},ant:{keywords:["animal","insect","nature","bug"],char:'🐜',fitzpatrick_scale:false,category:"animals_and_nature"},grasshopper:{keywords:["animal","cricket","chirp"],char:'🦗',fitzpatrick_scale:false,category:"animals_and_nature"},spider:{keywords:["animal","arachnid"],char:'🕷',fitzpatrick_scale:false,category:"animals_and_nature"},scorpion:{keywords:["animal","arachnid"],char:'🦂',fitzpatrick_scale:false,category:"animals_and_nature"},crab:{keywords:["animal","crustacean"],char:'🦀',fitzpatrick_scale:false,category:"animals_and_nature"},snake:{keywords:["animal","evil","nature","hiss","python"],char:'🐍',fitzpatrick_scale:false,category:"animals_and_nature"},lizard:{keywords:["animal","nature","reptile"],char:'🦎',fitzpatrick_scale:false,category:"animals_and_nature"},"t-rex":{keywords:["animal","nature","dinosaur","tyrannosaurus","extinct"],char:'🦖',fitzpatrick_scale:false,category:"animals_and_nature"},sauropod:{keywords:["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"],char:'🦕',fitzpatrick_scale:false,category:"animals_and_nature"},turtle:{keywords:["animal","slow","nature","tortoise"],char:'🐢',fitzpatrick_scale:false,category:"animals_and_nature"},tropical_fish:{keywords:["animal","swim","ocean","beach","nemo"],char:'🐠',fitzpatrick_scale:false,category:"animals_and_nature"},fish:{keywords:["animal","food","nature"],char:'🐟',fitzpatrick_scale:false,category:"animals_and_nature"},blowfish:{keywords:["animal","nature","food","sea","ocean"],char:'🐡',fitzpatrick_scale:false,category:"animals_and_nature"},dolphin:{keywords:["animal","nature","fish","sea","ocean","flipper","fins","beach"],char:'🐬',fitzpatrick_scale:false,category:"animals_and_nature"},shark:{keywords:["animal","nature","fish","sea","ocean","jaws","fins","beach"],char:'🦈',fitzpatrick_scale:false,category:"animals_and_nature"},whale:{keywords:["animal","nature","sea","ocean"],char:'🐳',fitzpatrick_scale:false,category:"animals_and_nature"},whale2:{keywords:["animal","nature","sea","ocean"],char:'🐋',fitzpatrick_scale:false,category:"animals_and_nature"},crocodile:{keywords:["animal","nature","reptile","lizard","alligator"],char:'🐊',fitzpatrick_scale:false,category:"animals_and_nature"},leopard:{keywords:["animal","nature"],char:'🐆',fitzpatrick_scale:false,category:"animals_and_nature"},zebra:{keywords:["animal","nature","stripes","safari"],char:'🦓',fitzpatrick_scale:false,category:"animals_and_nature"},tiger2:{keywords:["animal","nature","roar"],char:'🐅',fitzpatrick_scale:false,category:"animals_and_nature"},water_buffalo:{keywords:["animal","nature","ox","cow"],char:'🐃',fitzpatrick_scale:false,category:"animals_and_nature"},ox:{keywords:["animal","cow","beef"],char:'🐂',fitzpatrick_scale:false,category:"animals_and_nature"},cow2:{keywords:["beef","ox","animal","nature","moo","milk"],char:'🐄',fitzpatrick_scale:false,category:"animals_and_nature"},deer:{keywords:["animal","nature","horns","venison"],char:'🦌',fitzpatrick_scale:false,category:"animals_and_nature"},dromedary_camel:{keywords:["animal","hot","desert","hump"],char:'🐪',fitzpatrick_scale:false,category:"animals_and_nature"},camel:{keywords:["animal","nature","hot","desert","hump"],char:'🐫',fitzpatrick_scale:false,category:"animals_and_nature"},giraffe:{keywords:["animal","nature","spots","safari"],char:'🦒',fitzpatrick_scale:false,category:"animals_and_nature"},elephant:{keywords:["animal","nature","nose","th","circus"],char:'🐘',fitzpatrick_scale:false,category:"animals_and_nature"},rhinoceros:{keywords:["animal","nature","horn"],char:'🦏',fitzpatrick_scale:false,category:"animals_and_nature"},goat:{keywords:["animal","nature"],char:'🐐',fitzpatrick_scale:false,category:"animals_and_nature"},ram:{keywords:["animal","sheep","nature"],char:'🐏',fitzpatrick_scale:false,category:"animals_and_nature"},sheep:{keywords:["animal","nature","wool","shipit"],char:'🐑',fitzpatrick_scale:false,category:"animals_and_nature"},racehorse:{keywords:["animal","gamble","luck"],char:'🐎',fitzpatrick_scale:false,category:"animals_and_nature"},pig2:{keywords:["animal","nature"],char:'🐖',fitzpatrick_scale:false,category:"animals_and_nature"},rat:{keywords:["animal","mouse","rodent"],char:'🐀',fitzpatrick_scale:false,category:"animals_and_nature"},mouse2:{keywords:["animal","nature","rodent"],char:'🐁',fitzpatrick_scale:false,category:"animals_and_nature"},rooster:{keywords:["animal","nature","chicken"],char:'🐓',fitzpatrick_scale:false,category:"animals_and_nature"},turkey:{keywords:["animal","bird"],char:'🦃',fitzpatrick_scale:false,category:"animals_and_nature"},dove:{keywords:["animal","bird"],char:'🕊',fitzpatrick_scale:false,category:"animals_and_nature"},dog2:{keywords:["animal","nature","friend","doge","pet","faithful"],char:'🐕',fitzpatrick_scale:false,category:"animals_and_nature"},poodle:{keywords:["dog","animal","101","nature","pet"],char:'🐩',fitzpatrick_scale:false,category:"animals_and_nature"},cat2:{keywords:["animal","meow","pet","cats"],char:'🐈',fitzpatrick_scale:false,category:"animals_and_nature"},rabbit2:{keywords:["animal","nature","pet","magic","spring"],char:'🐇',fitzpatrick_scale:false,category:"animals_and_nature"},chipmunk:{keywords:["animal","nature","rodent","squirrel"],char:'🐿',fitzpatrick_scale:false,category:"animals_and_nature"},hedgehog:{keywords:["animal","nature","spiny"],char:'🦔',fitzpatrick_scale:false,category:"animals_and_nature"},raccoon:{keywords:["animal","nature"],char:'🦝',fitzpatrick_scale:false,category:"animals_and_nature"},llama:{keywords:["animal","nature","alpaca"],char:'🦙',fitzpatrick_scale:false,category:"animals_and_nature"},hippopotamus:{keywords:["animal","nature"],char:'🦛',fitzpatrick_scale:false,category:"animals_and_nature"},kangaroo:{keywords:["animal","nature","australia","joey","hop","marsupial"],char:'🦘',fitzpatrick_scale:false,category:"animals_and_nature"},badger:{keywords:["animal","nature","honey"],char:'🦡',fitzpatrick_scale:false,category:"animals_and_nature"},swan:{keywords:["animal","nature","bird"],char:'🦢',fitzpatrick_scale:false,category:"animals_and_nature"},peacock:{keywords:["animal","nature","peahen","bird"],char:'🦚',fitzpatrick_scale:false,category:"animals_and_nature"},parrot:{keywords:["animal","nature","bird","pirate","talk"],char:'🦜',fitzpatrick_scale:false,category:"animals_and_nature"},lobster:{keywords:["animal","nature","bisque","claws","seafood"],char:'🦞',fitzpatrick_scale:false,category:"animals_and_nature"},mosquito:{keywords:["animal","nature","insect","malaria"],char:'🦟',fitzpatrick_scale:false,category:"animals_and_nature"},paw_prints:{keywords:["animal","tracking","footprints","dog","cat","pet","feet"],char:'🐾',fitzpatrick_scale:false,category:"animals_and_nature"},dragon:{keywords:["animal","myth","nature","chinese","green"],char:'🐉',fitzpatrick_scale:false,category:"animals_and_nature"},dragon_face:{keywords:["animal","myth","nature","chinese","green"],char:'🐲',fitzpatrick_scale:false,category:"animals_and_nature"},cactus:{keywords:["vegetable","plant","nature"],char:'🌵',fitzpatrick_scale:false,category:"animals_and_nature"},christmas_tree:{keywords:["festival","vacation","december","xmas","celebration"],char:'🎄',fitzpatrick_scale:false,category:"animals_and_nature"},evergreen_tree:{keywords:["plant","nature"],char:'🌲',fitzpatrick_scale:false,category:"animals_and_nature"},deciduous_tree:{keywords:["plant","nature"],char:'🌳',fitzpatrick_scale:false,category:"animals_and_nature"},palm_tree:{keywords:["plant","vegetable","nature","summer","beach","mojito","tropical"],char:'🌴',fitzpatrick_scale:false,category:"animals_and_nature"},seedling:{keywords:["plant","nature","grass","lawn","spring"],char:'🌱',fitzpatrick_scale:false,category:"animals_and_nature"},herb:{keywords:["vegetable","plant","medicine","weed","grass","lawn"],char:'🌿',fitzpatrick_scale:false,category:"animals_and_nature"},shamrock:{keywords:["vegetable","plant","nature","irish","clover"],char:'☘',fitzpatrick_scale:false,category:"animals_and_nature"},four_leaf_clover:{keywords:["vegetable","plant","nature","lucky","irish"],char:'🍀',fitzpatrick_scale:false,category:"animals_and_nature"},bamboo:{keywords:["plant","nature","vegetable","panda","pine_decoration"],char:'🎍',fitzpatrick_scale:false,category:"animals_and_nature"},tanabata_tree:{keywords:["plant","nature","branch","summer"],char:'🎋',fitzpatrick_scale:false,category:"animals_and_nature"},leaves:{keywords:["nature","plant","tree","vegetable","grass","lawn","spring"],char:'🍃',fitzpatrick_scale:false,category:"animals_and_nature"},fallen_leaf:{keywords:["nature","plant","vegetable","leaves"],char:'🍂',fitzpatrick_scale:false,category:"animals_and_nature"},maple_leaf:{keywords:["nature","plant","vegetable","ca","fall"],char:'🍁',fitzpatrick_scale:false,category:"animals_and_nature"},ear_of_rice:{keywords:["nature","plant"],char:'🌾',fitzpatrick_scale:false,category:"animals_and_nature"},hibiscus:{keywords:["plant","vegetable","flowers","beach"],char:'🌺',fitzpatrick_scale:false,category:"animals_and_nature"},sunflower:{keywords:["nature","plant","fall"],char:'🌻',fitzpatrick_scale:false,category:"animals_and_nature"},rose:{keywords:["flowers","valentines","love","spring"],char:'🌹',fitzpatrick_scale:false,category:"animals_and_nature"},wilted_flower:{keywords:["plant","nature","flower"],char:'🥀',fitzpatrick_scale:false,category:"animals_and_nature"},tulip:{keywords:["flowers","plant","nature","summer","spring"],char:'🌷',fitzpatrick_scale:false,category:"animals_and_nature"},blossom:{keywords:["nature","flowers","yellow"],char:'🌼',fitzpatrick_scale:false,category:"animals_and_nature"},cherry_blossom:{keywords:["nature","plant","spring","flower"],char:'🌸',fitzpatrick_scale:false,category:"animals_and_nature"},bouquet:{keywords:["flowers","nature","spring"],char:'💐',fitzpatrick_scale:false,category:"animals_and_nature"},mushroom:{keywords:["plant","vegetable"],char:'🍄',fitzpatrick_scale:false,category:"animals_and_nature"},chestnut:{keywords:["food","squirrel"],char:'🌰',fitzpatrick_scale:false,category:"animals_and_nature"},jack_o_lantern:{keywords:["halloween","light","pumpkin","creepy","fall"],char:'🎃',fitzpatrick_scale:false,category:"animals_and_nature"},shell:{keywords:["nature","sea","beach"],char:'🐚',fitzpatrick_scale:false,category:"animals_and_nature"},spider_web:{keywords:["animal","insect","arachnid","silk"],char:'🕸',fitzpatrick_scale:false,category:"animals_and_nature"},earth_americas:{keywords:["globe","world","USA","international"],char:'🌎',fitzpatrick_scale:false,category:"animals_and_nature"},earth_africa:{keywords:["globe","world","international"],char:'🌍',fitzpatrick_scale:false,category:"animals_and_nature"},earth_asia:{keywords:["globe","world","east","international"],char:'🌏',fitzpatrick_scale:false,category:"animals_and_nature"},full_moon:{keywords:["nature","yellow","twilight","planet","space","night","evening","sleep"],char:'🌕',fitzpatrick_scale:false,category:"animals_and_nature"},waning_gibbous_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"],char:'🌖',fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌗',fitzpatrick_scale:false,category:"animals_and_nature"},waning_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌘',fitzpatrick_scale:false,category:"animals_and_nature"},new_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌑',fitzpatrick_scale:false,category:"animals_and_nature"},waxing_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌒',fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌓',fitzpatrick_scale:false,category:"animals_and_nature"},waxing_gibbous_moon:{keywords:["nature","night","sky","gray","twilight","planet","space","evening","sleep"],char:'🌔',fitzpatrick_scale:false,category:"animals_and_nature"},new_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌚',fitzpatrick_scale:false,category:"animals_and_nature"},full_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌝',fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌛',fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'🌜',fitzpatrick_scale:false,category:"animals_and_nature"},sun_with_face:{keywords:["nature","morning","sky"],char:'🌞',fitzpatrick_scale:false,category:"animals_and_nature"},crescent_moon:{keywords:["night","sleep","sky","evening","magic"],char:'🌙',fitzpatrick_scale:false,category:"animals_and_nature"},star:{keywords:["night","yellow"],char:'⭐',fitzpatrick_scale:false,category:"animals_and_nature"},star2:{keywords:["night","sparkle","awesome","good","magic"],char:'🌟',fitzpatrick_scale:false,category:"animals_and_nature"},dizzy:{keywords:["star","sparkle","shoot","magic"],char:'💫',fitzpatrick_scale:false,category:"animals_and_nature"},sparkles:{keywords:["stars","shine","shiny","cool","awesome","good","magic"],char:'✨',fitzpatrick_scale:false,category:"animals_and_nature"},comet:{keywords:["space"],char:'☄',fitzpatrick_scale:false,category:"animals_and_nature"},sunny:{keywords:["weather","nature","brightness","summer","beach","spring"],char:'☀️',fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_small_cloud:{keywords:["weather"],char:'🌤',fitzpatrick_scale:false,category:"animals_and_nature"},partly_sunny:{keywords:["weather","nature","cloudy","morning","fall","spring"],char:'⛅',fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_large_cloud:{keywords:["weather"],char:'🌥',fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_rain_cloud:{keywords:["weather"],char:'🌦',fitzpatrick_scale:false,category:"animals_and_nature"},cloud:{keywords:["weather","sky"],char:'☁️',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_rain:{keywords:["weather"],char:'🌧',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning_and_rain:{keywords:["weather","lightning"],char:'⛈',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning:{keywords:["weather","thunder"],char:'🌩',fitzpatrick_scale:false,category:"animals_and_nature"},zap:{keywords:["thunder","weather","lightning bolt","fast"],char:'⚡',fitzpatrick_scale:false,category:"animals_and_nature"},fire:{keywords:["hot","cook","flame"],char:'🔥',fitzpatrick_scale:false,category:"animals_and_nature"},boom:{keywords:["bomb","explode","explosion","collision","blown"],char:'💥',fitzpatrick_scale:false,category:"animals_and_nature"},snowflake:{keywords:["winter","season","cold","weather","christmas","xmas"],char:'❄️',fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_snow:{keywords:["weather"],char:'🌨',fitzpatrick_scale:false,category:"animals_and_nature"},snowman:{keywords:["winter","season","cold","weather","christmas","xmas","frozen","without_snow"],char:'⛄',fitzpatrick_scale:false,category:"animals_and_nature"},snowman_with_snow:{keywords:["winter","season","cold","weather","christmas","xmas","frozen"],char:'☃',fitzpatrick_scale:false,category:"animals_and_nature"},wind_face:{keywords:["gust","air"],char:'🌬',fitzpatrick_scale:false,category:"animals_and_nature"},dash:{keywords:["wind","air","fast","shoo","fart","smoke","puff"],char:'💨',fitzpatrick_scale:false,category:"animals_and_nature"},tornado:{keywords:["weather","cyclone","twister"],char:'🌪',fitzpatrick_scale:false,category:"animals_and_nature"},fog:{keywords:["weather"],char:'🌫',fitzpatrick_scale:false,category:"animals_and_nature"},open_umbrella:{keywords:["weather","spring"],char:'☂',fitzpatrick_scale:false,category:"animals_and_nature"},umbrella:{keywords:["rainy","weather","spring"],char:'☔',fitzpatrick_scale:false,category:"animals_and_nature"},droplet:{keywords:["water","drip","faucet","spring"],char:'💧',fitzpatrick_scale:false,category:"animals_and_nature"},sweat_drops:{keywords:["water","drip","oops"],char:'💦',fitzpatrick_scale:false,category:"animals_and_nature"},ocean:{keywords:["sea","water","wave","nature","tsunami","disaster"],char:'🌊',fitzpatrick_scale:false,category:"animals_and_nature"},green_apple:{keywords:["fruit","nature"],char:'🍏',fitzpatrick_scale:false,category:"food_and_drink"},apple:{keywords:["fruit","mac","school"],char:'🍎',fitzpatrick_scale:false,category:"food_and_drink"},pear:{keywords:["fruit","nature","food"],char:'🍐',fitzpatrick_scale:false,category:"food_and_drink"},tangerine:{keywords:["food","fruit","nature","orange"],char:'🍊',fitzpatrick_scale:false,category:"food_and_drink"},lemon:{keywords:["fruit","nature"],char:'🍋',fitzpatrick_scale:false,category:"food_and_drink"},banana:{keywords:["fruit","food","monkey"],char:'🍌',fitzpatrick_scale:false,category:"food_and_drink"},watermelon:{keywords:["fruit","food","picnic","summer"],char:'🍉',fitzpatrick_scale:false,category:"food_and_drink"},grapes:{keywords:["fruit","food","wine"],char:'🍇',fitzpatrick_scale:false,category:"food_and_drink"},strawberry:{keywords:["fruit","food","nature"],char:'🍓',fitzpatrick_scale:false,category:"food_and_drink"},melon:{keywords:["fruit","nature","food"],char:'🍈',fitzpatrick_scale:false,category:"food_and_drink"},cherries:{keywords:["food","fruit"],char:'🍒',fitzpatrick_scale:false,category:"food_and_drink"},peach:{keywords:["fruit","nature","food"],char:'🍑',fitzpatrick_scale:false,category:"food_and_drink"},pineapple:{keywords:["fruit","nature","food"],char:'🍍',fitzpatrick_scale:false,category:"food_and_drink"},coconut:{keywords:["fruit","nature","food","palm"],char:'🥥',fitzpatrick_scale:false,category:"food_and_drink"},kiwi_fruit:{keywords:["fruit","food"],char:'🥝',fitzpatrick_scale:false,category:"food_and_drink"},mango:{keywords:["fruit","food","tropical"],char:'🥭',fitzpatrick_scale:false,category:"food_and_drink"},avocado:{keywords:["fruit","food"],char:'🥑',fitzpatrick_scale:false,category:"food_and_drink"},broccoli:{keywords:["fruit","food","vegetable"],char:'🥦',fitzpatrick_scale:false,category:"food_and_drink"},tomato:{keywords:["fruit","vegetable","nature","food"],char:'🍅',fitzpatrick_scale:false,category:"food_and_drink"},eggplant:{keywords:["vegetable","nature","food","aubergine"],char:'🍆',fitzpatrick_scale:false,category:"food_and_drink"},cucumber:{keywords:["fruit","food","pickle"],char:'🥒',fitzpatrick_scale:false,category:"food_and_drink"},carrot:{keywords:["vegetable","food","orange"],char:'🥕',fitzpatrick_scale:false,category:"food_and_drink"},hot_pepper:{keywords:["food","spicy","chilli","chili"],char:'🌶',fitzpatrick_scale:false,category:"food_and_drink"},potato:{keywords:["food","tuber","vegatable","starch"],char:'🥔',fitzpatrick_scale:false,category:"food_and_drink"},corn:{keywords:["food","vegetable","plant"],char:'🌽',fitzpatrick_scale:false,category:"food_and_drink"},leafy_greens:{keywords:["food","vegetable","plant","bok choy","cabbage","kale","lettuce"],char:'🥬',fitzpatrick_scale:false,category:"food_and_drink"},sweet_potato:{keywords:["food","nature"],char:'🍠',fitzpatrick_scale:false,category:"food_and_drink"},peanuts:{keywords:["food","nut"],char:'🥜',fitzpatrick_scale:false,category:"food_and_drink"},honey_pot:{keywords:["bees","sweet","kitchen"],char:'🍯',fitzpatrick_scale:false,category:"food_and_drink"},croissant:{keywords:["food","bread","french"],char:'🥐',fitzpatrick_scale:false,category:"food_and_drink"},bread:{keywords:["food","wheat","breakfast","toast"],char:'🍞',fitzpatrick_scale:false,category:"food_and_drink"},baguette_bread:{keywords:["food","bread","french"],char:'🥖',fitzpatrick_scale:false,category:"food_and_drink"},bagel:{keywords:["food","bread","bakery","schmear"],char:'🥯',fitzpatrick_scale:false,category:"food_and_drink"},pretzel:{keywords:["food","bread","twisted"],char:'🥨',fitzpatrick_scale:false,category:"food_and_drink"},cheese:{keywords:["food","chadder"],char:'🧀',fitzpatrick_scale:false,category:"food_and_drink"},egg:{keywords:["food","chicken","breakfast"],char:'🥚',fitzpatrick_scale:false,category:"food_and_drink"},bacon:{keywords:["food","breakfast","pork","pig","meat"],char:'🥓',fitzpatrick_scale:false,category:"food_and_drink"},steak:{keywords:["food","cow","meat","cut","chop","lambchop","porkchop"],char:'🥩',fitzpatrick_scale:false,category:"food_and_drink"},pancakes:{keywords:["food","breakfast","flapjacks","hotcakes"],char:'🥞',fitzpatrick_scale:false,category:"food_and_drink"},poultry_leg:{keywords:["food","meat","drumstick","bird","chicken","turkey"],char:'🍗',fitzpatrick_scale:false,category:"food_and_drink"},meat_on_bone:{keywords:["good","food","drumstick"],char:'🍖',fitzpatrick_scale:false,category:"food_and_drink"},bone:{keywords:["skeleton"],char:'🦴',fitzpatrick_scale:false,category:"food_and_drink"},fried_shrimp:{keywords:["food","animal","appetizer","summer"],char:'🍤',fitzpatrick_scale:false,category:"food_and_drink"},fried_egg:{keywords:["food","breakfast","kitchen","egg"],char:'🍳',fitzpatrick_scale:false,category:"food_and_drink"},hamburger:{keywords:["meat","fast food","beef","cheeseburger","mcdonalds","burger king"],char:'🍔',fitzpatrick_scale:false,category:"food_and_drink"},fries:{keywords:["chips","snack","fast food"],char:'🍟',fitzpatrick_scale:false,category:"food_and_drink"},stuffed_flatbread:{keywords:["food","flatbread","stuffed","gyro"],char:'🥙',fitzpatrick_scale:false,category:"food_and_drink"},hotdog:{keywords:["food","frankfurter"],char:'🌭',fitzpatrick_scale:false,category:"food_and_drink"},pizza:{keywords:["food","party"],char:'🍕',fitzpatrick_scale:false,category:"food_and_drink"},sandwich:{keywords:["food","lunch","bread"],char:'🥪',fitzpatrick_scale:false,category:"food_and_drink"},canned_food:{keywords:["food","soup"],char:'🥫',fitzpatrick_scale:false,category:"food_and_drink"},spaghetti:{keywords:["food","italian","noodle"],char:'🍝',fitzpatrick_scale:false,category:"food_and_drink"},taco:{keywords:["food","mexican"],char:'🌮',fitzpatrick_scale:false,category:"food_and_drink"},burrito:{keywords:["food","mexican"],char:'🌯',fitzpatrick_scale:false,category:"food_and_drink"},green_salad:{keywords:["food","healthy","lettuce"],char:'🥗',fitzpatrick_scale:false,category:"food_and_drink"},shallow_pan_of_food:{keywords:["food","cooking","casserole","paella"],char:'🥘',fitzpatrick_scale:false,category:"food_and_drink"},ramen:{keywords:["food","japanese","noodle","chopsticks"],char:'🍜',fitzpatrick_scale:false,category:"food_and_drink"},stew:{keywords:["food","meat","soup"],char:'🍲',fitzpatrick_scale:false,category:"food_and_drink"},fish_cake:{keywords:["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"],char:'🍥',fitzpatrick_scale:false,category:"food_and_drink"},fortune_cookie:{keywords:["food","prophecy"],char:'🥠',fitzpatrick_scale:false,category:"food_and_drink"},sushi:{keywords:["food","fish","japanese","rice"],char:'🍣',fitzpatrick_scale:false,category:"food_and_drink"},bento:{keywords:["food","japanese","box"],char:'🍱',fitzpatrick_scale:false,category:"food_and_drink"},curry:{keywords:["food","spicy","hot","indian"],char:'🍛',fitzpatrick_scale:false,category:"food_and_drink"},rice_ball:{keywords:["food","japanese"],char:'🍙',fitzpatrick_scale:false,category:"food_and_drink"},rice:{keywords:["food","china","asian"],char:'🍚',fitzpatrick_scale:false,category:"food_and_drink"},rice_cracker:{keywords:["food","japanese"],char:'🍘',fitzpatrick_scale:false,category:"food_and_drink"},oden:{keywords:["food","japanese"],char:'🍢',fitzpatrick_scale:false,category:"food_and_drink"},dango:{keywords:["food","dessert","sweet","japanese","barbecue","meat"],char:'🍡',fitzpatrick_scale:false,category:"food_and_drink"},shaved_ice:{keywords:["hot","dessert","summer"],char:'🍧',fitzpatrick_scale:false,category:"food_and_drink"},ice_cream:{keywords:["food","hot","dessert"],char:'🍨',fitzpatrick_scale:false,category:"food_and_drink"},icecream:{keywords:["food","hot","dessert","summer"],char:'🍦',fitzpatrick_scale:false,category:"food_and_drink"},pie:{keywords:["food","dessert","pastry"],char:'🥧',fitzpatrick_scale:false,category:"food_and_drink"},cake:{keywords:["food","dessert"],char:'🍰',fitzpatrick_scale:false,category:"food_and_drink"},cupcake:{keywords:["food","dessert","bakery","sweet"],char:'🧁',fitzpatrick_scale:false,category:"food_and_drink"},moon_cake:{keywords:["food","autumn"],char:'🥮',fitzpatrick_scale:false,category:"food_and_drink"},birthday:{keywords:["food","dessert","cake"],char:'🎂',fitzpatrick_scale:false,category:"food_and_drink"},custard:{keywords:["dessert","food"],char:'🍮',fitzpatrick_scale:false,category:"food_and_drink"},candy:{keywords:["snack","dessert","sweet","lolly"],char:'🍬',fitzpatrick_scale:false,category:"food_and_drink"},lollipop:{keywords:["food","snack","candy","sweet"],char:'🍭',fitzpatrick_scale:false,category:"food_and_drink"},chocolate_bar:{keywords:["food","snack","dessert","sweet"],char:'🍫',fitzpatrick_scale:false,category:"food_and_drink"},popcorn:{keywords:["food","movie theater","films","snack"],char:'🍿',fitzpatrick_scale:false,category:"food_and_drink"},dumpling:{keywords:["food","empanada","pierogi","potsticker"],char:'🥟',fitzpatrick_scale:false,category:"food_and_drink"},doughnut:{keywords:["food","dessert","snack","sweet","donut"],char:'🍩',fitzpatrick_scale:false,category:"food_and_drink"},cookie:{keywords:["food","snack","oreo","chocolate","sweet","dessert"],char:'🍪',fitzpatrick_scale:false,category:"food_and_drink"},milk_glass:{keywords:["beverage","drink","cow"],char:'🥛',fitzpatrick_scale:false,category:"food_and_drink"},beer:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:'🍺',fitzpatrick_scale:false,category:"food_and_drink"},beers:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:'🍻',fitzpatrick_scale:false,category:"food_and_drink"},clinking_glasses:{keywords:["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"],char:'🥂',fitzpatrick_scale:false,category:"food_and_drink"},wine_glass:{keywords:["drink","beverage","drunk","alcohol","booze"],char:'🍷',fitzpatrick_scale:false,category:"food_and_drink"},tumbler_glass:{keywords:["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"],char:'🥃',fitzpatrick_scale:false,category:"food_and_drink"},cocktail:{keywords:["drink","drunk","alcohol","beverage","booze","mojito"],char:'🍸',fitzpatrick_scale:false,category:"food_and_drink"},tropical_drink:{keywords:["beverage","cocktail","summer","beach","alcohol","booze","mojito"],char:'🍹',fitzpatrick_scale:false,category:"food_and_drink"},champagne:{keywords:["drink","wine","bottle","celebration"],char:'🍾',fitzpatrick_scale:false,category:"food_and_drink"},sake:{keywords:["wine","drink","drunk","beverage","japanese","alcohol","booze"],char:'🍶',fitzpatrick_scale:false,category:"food_and_drink"},tea:{keywords:["drink","bowl","breakfast","green","british"],char:'🍵',fitzpatrick_scale:false,category:"food_and_drink"},cup_with_straw:{keywords:["drink","soda"],char:'🥤',fitzpatrick_scale:false,category:"food_and_drink"},coffee:{keywords:["beverage","caffeine","latte","espresso"],char:'☕',fitzpatrick_scale:false,category:"food_and_drink"},baby_bottle:{keywords:["food","container","milk"],char:'🍼',fitzpatrick_scale:false,category:"food_and_drink"},salt:{keywords:["condiment","shaker"],char:'🧂',fitzpatrick_scale:false,category:"food_and_drink"},spoon:{keywords:["cutlery","kitchen","tableware"],char:'🥄',fitzpatrick_scale:false,category:"food_and_drink"},fork_and_knife:{keywords:["cutlery","kitchen"],char:'🍴',fitzpatrick_scale:false,category:"food_and_drink"},plate_with_cutlery:{keywords:["food","eat","meal","lunch","dinner","restaurant"],char:'🍽',fitzpatrick_scale:false,category:"food_and_drink"},bowl_with_spoon:{keywords:["food","breakfast","cereal","oatmeal","porridge"],char:'🥣',fitzpatrick_scale:false,category:"food_and_drink"},takeout_box:{keywords:["food","leftovers"],char:'🥡',fitzpatrick_scale:false,category:"food_and_drink"},chopsticks:{keywords:["food"],char:'🥢',fitzpatrick_scale:false,category:"food_and_drink"},soccer:{keywords:["sports","football"],char:'⚽',fitzpatrick_scale:false,category:"activity"},basketball:{keywords:["sports","balls","NBA"],char:'🏀',fitzpatrick_scale:false,category:"activity"},football:{keywords:["sports","balls","NFL"],char:'🏈',fitzpatrick_scale:false,category:"activity"},baseball:{keywords:["sports","balls"],char:'⚾',fitzpatrick_scale:false,category:"activity"},softball:{keywords:["sports","balls"],char:'🥎',fitzpatrick_scale:false,category:"activity"},tennis:{keywords:["sports","balls","green"],char:'🎾',fitzpatrick_scale:false,category:"activity"},volleyball:{keywords:["sports","balls"],char:'🏐',fitzpatrick_scale:false,category:"activity"},rugby_football:{keywords:["sports","team"],char:'🏉',fitzpatrick_scale:false,category:"activity"},flying_disc:{keywords:["sports","frisbee","ultimate"],char:'🥏',fitzpatrick_scale:false,category:"activity"},"8ball":{keywords:["pool","hobby","game","luck","magic"],char:'🎱',fitzpatrick_scale:false,category:"activity"},golf:{keywords:["sports","business","flag","hole","summer"],char:'⛳',fitzpatrick_scale:false,category:"activity"},golfing_woman:{keywords:["sports","business","woman","female"],char:'🏌️‍♀️',fitzpatrick_scale:false,category:"activity"},golfing_man:{keywords:["sports","business"],char:'🏌',fitzpatrick_scale:true,category:"activity"},ping_pong:{keywords:["sports","pingpong"],char:'🏓',fitzpatrick_scale:false,category:"activity"},badminton:{keywords:["sports"],char:'🏸',fitzpatrick_scale:false,category:"activity"},goal_net:{keywords:["sports"],char:'🥅',fitzpatrick_scale:false,category:"activity"},ice_hockey:{keywords:["sports"],char:'🏒',fitzpatrick_scale:false,category:"activity"},field_hockey:{keywords:["sports"],char:'🏑',fitzpatrick_scale:false,category:"activity"},lacrosse:{keywords:["sports","ball","stick"],char:'🥍',fitzpatrick_scale:false,category:"activity"},cricket:{keywords:["sports"],char:'🏏',fitzpatrick_scale:false,category:"activity"},ski:{keywords:["sports","winter","cold","snow"],char:'🎿',fitzpatrick_scale:false,category:"activity"},skier:{keywords:["sports","winter","snow"],char:'⛷',fitzpatrick_scale:false,category:"activity"},snowboarder:{keywords:["sports","winter"],char:'🏂',fitzpatrick_scale:true,category:"activity"},person_fencing:{keywords:["sports","fencing","sword"],char:'🤺',fitzpatrick_scale:false,category:"activity"},women_wrestling:{keywords:["sports","wrestlers"],char:'🤼‍♀️',fitzpatrick_scale:false,category:"activity"},men_wrestling:{keywords:["sports","wrestlers"],char:'🤼‍♂️',fitzpatrick_scale:false,category:"activity"},woman_cartwheeling:{keywords:["gymnastics"],char:'🤸‍♀️',fitzpatrick_scale:true,category:"activity"},man_cartwheeling:{keywords:["gymnastics"],char:'🤸‍♂️',fitzpatrick_scale:true,category:"activity"},woman_playing_handball:{keywords:["sports"],char:'🤾‍♀️',fitzpatrick_scale:true,category:"activity"},man_playing_handball:{keywords:["sports"],char:'🤾‍♂️',fitzpatrick_scale:true,category:"activity"},ice_skate:{keywords:["sports"],char:'⛸',fitzpatrick_scale:false,category:"activity"},curling_stone:{keywords:["sports"],char:'🥌',fitzpatrick_scale:false,category:"activity"},skateboard:{keywords:["board"],char:'🛹',fitzpatrick_scale:false,category:"activity"},sled:{keywords:["sleigh","luge","toboggan"],char:'🛷',fitzpatrick_scale:false,category:"activity"},bow_and_arrow:{keywords:["sports"],char:'🏹',fitzpatrick_scale:false,category:"activity"},fishing_pole_and_fish:{keywords:["food","hobby","summer"],char:'🎣',fitzpatrick_scale:false,category:"activity"},boxing_glove:{keywords:["sports","fighting"],char:'🥊',fitzpatrick_scale:false,category:"activity"},martial_arts_uniform:{keywords:["judo","karate","taekwondo"],char:'🥋',fitzpatrick_scale:false,category:"activity"},rowing_woman:{keywords:["sports","hobby","water","ship","woman","female"],char:'🚣‍♀️',fitzpatrick_scale:true,category:"activity"},rowing_man:{keywords:["sports","hobby","water","ship"],char:'🚣',fitzpatrick_scale:true,category:"activity"},climbing_woman:{keywords:["sports","hobby","woman","female","rock"],char:'🧗‍♀️',fitzpatrick_scale:true,category:"activity"},climbing_man:{keywords:["sports","hobby","man","male","rock"],char:'🧗‍♂️',fitzpatrick_scale:true,category:"activity"},swimming_woman:{keywords:["sports","exercise","human","athlete","water","summer","woman","female"],char:'🏊‍♀️',fitzpatrick_scale:true,category:"activity"},swimming_man:{keywords:["sports","exercise","human","athlete","water","summer"],char:'🏊',fitzpatrick_scale:true,category:"activity"},woman_playing_water_polo:{keywords:["sports","pool"],char:'🤽‍♀️',fitzpatrick_scale:true,category:"activity"},man_playing_water_polo:{keywords:["sports","pool"],char:'🤽‍♂️',fitzpatrick_scale:true,category:"activity"},woman_in_lotus_position:{keywords:["woman","female","meditation","yoga","serenity","zen","mindfulness"],char:'🧘‍♀️',fitzpatrick_scale:true,category:"activity"},man_in_lotus_position:{keywords:["man","male","meditation","yoga","serenity","zen","mindfulness"],char:'🧘‍♂️',fitzpatrick_scale:true,category:"activity"},surfing_woman:{keywords:["sports","ocean","sea","summer","beach","woman","female"],char:'🏄‍♀️',fitzpatrick_scale:true,category:"activity"},surfing_man:{keywords:["sports","ocean","sea","summer","beach"],char:'🏄',fitzpatrick_scale:true,category:"activity"},bath:{keywords:["clean","shower","bathroom"],char:'🛀',fitzpatrick_scale:true,category:"activity"},basketball_woman:{keywords:["sports","human","woman","female"],char:'⛹️‍♀️',fitzpatrick_scale:true,category:"activity"},basketball_man:{keywords:["sports","human"],char:'⛹',fitzpatrick_scale:true,category:"activity"},weight_lifting_woman:{keywords:["sports","training","exercise","woman","female"],char:'🏋️‍♀️',fitzpatrick_scale:true,category:"activity"},weight_lifting_man:{keywords:["sports","training","exercise"],char:'🏋',fitzpatrick_scale:true,category:"activity"},biking_woman:{keywords:["sports","bike","exercise","hipster","woman","female"],char:'🚴‍♀️',fitzpatrick_scale:true,category:"activity"},biking_man:{keywords:["sports","bike","exercise","hipster"],char:'🚴',fitzpatrick_scale:true,category:"activity"},mountain_biking_woman:{keywords:["transportation","sports","human","race","bike","woman","female"],char:'🚵‍♀️',fitzpatrick_scale:true,category:"activity"},mountain_biking_man:{keywords:["transportation","sports","human","race","bike"],char:'🚵',fitzpatrick_scale:true,category:"activity"},horse_racing:{keywords:["animal","betting","competition","gambling","luck"],char:'🏇',fitzpatrick_scale:true,category:"activity"},business_suit_levitating:{keywords:["suit","business","levitate","hover","jump"],char:'🕴',fitzpatrick_scale:true,category:"activity"},trophy:{keywords:["win","award","contest","place","ftw","ceremony"],char:'🏆',fitzpatrick_scale:false,category:"activity"},running_shirt_with_sash:{keywords:["play","pageant"],char:'🎽',fitzpatrick_scale:false,category:"activity"},medal_sports:{keywords:["award","winning"],char:'🏅',fitzpatrick_scale:false,category:"activity"},medal_military:{keywords:["award","winning","army"],char:'🎖',fitzpatrick_scale:false,category:"activity"},"1st_place_medal":{keywords:["award","winning","first"],char:'🥇',fitzpatrick_scale:false,category:"activity"},"2nd_place_medal":{keywords:["award","second"],char:'🥈',fitzpatrick_scale:false,category:"activity"},"3rd_place_medal":{keywords:["award","third"],char:'🥉',fitzpatrick_scale:false,category:"activity"},reminder_ribbon:{keywords:["sports","cause","support","awareness"],char:'🎗',fitzpatrick_scale:false,category:"activity"},rosette:{keywords:["flower","decoration","military"],char:'🏵',fitzpatrick_scale:false,category:"activity"},ticket:{keywords:["event","concert","pass"],char:'🎫',fitzpatrick_scale:false,category:"activity"},tickets:{keywords:["sports","concert","entrance"],char:'🎟',fitzpatrick_scale:false,category:"activity"},performing_arts:{keywords:["acting","theater","drama"],char:'🎭',fitzpatrick_scale:false,category:"activity"},art:{keywords:["design","paint","draw","colors"],char:'🎨',fitzpatrick_scale:false,category:"activity"},circus_tent:{keywords:["festival","carnival","party"],char:'🎪',fitzpatrick_scale:false,category:"activity"},woman_juggling:{keywords:["juggle","balance","skill","multitask"],char:'🤹‍♀️',fitzpatrick_scale:true,category:"activity"},man_juggling:{keywords:["juggle","balance","skill","multitask"],char:'🤹‍♂️',fitzpatrick_scale:true,category:"activity"},microphone:{keywords:["sound","music","PA","sing","talkshow"],char:'🎤',fitzpatrick_scale:false,category:"activity"},headphones:{keywords:["music","score","gadgets"],char:'🎧',fitzpatrick_scale:false,category:"activity"},musical_score:{keywords:["treble","clef","compose"],char:'🎼',fitzpatrick_scale:false,category:"activity"},musical_keyboard:{keywords:["piano","instrument","compose"],char:'🎹',fitzpatrick_scale:false,category:"activity"},drum:{keywords:["music","instrument","drumsticks","snare"],char:'🥁',fitzpatrick_scale:false,category:"activity"},saxophone:{keywords:["music","instrument","jazz","blues"],char:'🎷',fitzpatrick_scale:false,category:"activity"},trumpet:{keywords:["music","brass"],char:'🎺',fitzpatrick_scale:false,category:"activity"},guitar:{keywords:["music","instrument"],char:'🎸',fitzpatrick_scale:false,category:"activity"},violin:{keywords:["music","instrument","orchestra","symphony"],char:'🎻',fitzpatrick_scale:false,category:"activity"},clapper:{keywords:["movie","film","record"],char:'🎬',fitzpatrick_scale:false,category:"activity"},video_game:{keywords:["play","console","PS4","controller"],char:'🎮',fitzpatrick_scale:false,category:"activity"},space_invader:{keywords:["game","arcade","play"],char:'👾',fitzpatrick_scale:false,category:"activity"},dart:{keywords:["game","play","bar","target","bullseye"],char:'🎯',fitzpatrick_scale:false,category:"activity"},game_die:{keywords:["dice","random","tabletop","play","luck"],char:'🎲',fitzpatrick_scale:false,category:"activity"},chess_pawn:{keywords:["expendable"],char:"♟",fitzpatrick_scale:false,category:"activity"},slot_machine:{keywords:["bet","gamble","vegas","fruit machine","luck","casino"],char:'🎰',fitzpatrick_scale:false,category:"activity"},jigsaw:{keywords:["interlocking","puzzle","piece"],char:'🧩',fitzpatrick_scale:false,category:"activity"},bowling:{keywords:["sports","fun","play"],char:'🎳',fitzpatrick_scale:false,category:"activity"},red_car:{keywords:["red","transportation","vehicle"],char:'🚗',fitzpatrick_scale:false,category:"travel_and_places"},taxi:{keywords:["uber","vehicle","cars","transportation"],char:'🚕',fitzpatrick_scale:false,category:"travel_and_places"},blue_car:{keywords:["transportation","vehicle"],char:'🚙',fitzpatrick_scale:false,category:"travel_and_places"},bus:{keywords:["car","vehicle","transportation"],char:'🚌',fitzpatrick_scale:false,category:"travel_and_places"},trolleybus:{keywords:["bart","transportation","vehicle"],char:'🚎',fitzpatrick_scale:false,category:"travel_and_places"},racing_car:{keywords:["sports","race","fast","formula","f1"],char:'🏎',fitzpatrick_scale:false,category:"travel_and_places"},police_car:{keywords:["vehicle","cars","transportation","law","legal","enforcement"],char:'🚓',fitzpatrick_scale:false,category:"travel_and_places"},ambulance:{keywords:["health","911","hospital"],char:'🚑',fitzpatrick_scale:false,category:"travel_and_places"},fire_engine:{keywords:["transportation","cars","vehicle"],char:'🚒',fitzpatrick_scale:false,category:"travel_and_places"},minibus:{keywords:["vehicle","car","transportation"],char:'🚐',fitzpatrick_scale:false,category:"travel_and_places"},truck:{keywords:["cars","transportation"],char:'🚚',fitzpatrick_scale:false,category:"travel_and_places"},articulated_lorry:{keywords:["vehicle","cars","transportation","express"],char:'🚛',fitzpatrick_scale:false,category:"travel_and_places"},tractor:{keywords:["vehicle","car","farming","agriculture"],char:'🚜',fitzpatrick_scale:false,category:"travel_and_places"},kick_scooter:{keywords:["vehicle","kick","razor"],char:'🛴',fitzpatrick_scale:false,category:"travel_and_places"},motorcycle:{keywords:["race","sports","fast"],char:'🏍',fitzpatrick_scale:false,category:"travel_and_places"},bike:{keywords:["sports","bicycle","exercise","hipster"],char:'🚲',fitzpatrick_scale:false,category:"travel_and_places"},motor_scooter:{keywords:["vehicle","vespa","sasha"],char:'🛵',fitzpatrick_scale:false,category:"travel_and_places"},rotating_light:{keywords:["police","ambulance","911","emergency","alert","error","pinged","law","legal"],char:'🚨',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_police_car:{keywords:["vehicle","law","legal","enforcement","911"],char:'🚔',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_bus:{keywords:["vehicle","transportation"],char:'🚍',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_automobile:{keywords:["car","vehicle","transportation"],char:'🚘',fitzpatrick_scale:false,category:"travel_and_places"},oncoming_taxi:{keywords:["vehicle","cars","uber"],char:'🚖',fitzpatrick_scale:false,category:"travel_and_places"},aerial_tramway:{keywords:["transportation","vehicle","ski"],char:'🚡',fitzpatrick_scale:false,category:"travel_and_places"},mountain_cableway:{keywords:["transportation","vehicle","ski"],char:'🚠',fitzpatrick_scale:false,category:"travel_and_places"},suspension_railway:{keywords:["vehicle","transportation"],char:'🚟',fitzpatrick_scale:false,category:"travel_and_places"},railway_car:{keywords:["transportation","vehicle"],char:'🚃',fitzpatrick_scale:false,category:"travel_and_places"},train:{keywords:["transportation","vehicle","carriage","public","travel"],char:'🚋',fitzpatrick_scale:false,category:"travel_and_places"},monorail:{keywords:["transportation","vehicle"],char:'🚝',fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_side:{keywords:["transportation","vehicle"],char:'🚄',fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_front:{keywords:["transportation","vehicle","speed","fast","public","travel"],char:'🚅',fitzpatrick_scale:false,category:"travel_and_places"},light_rail:{keywords:["transportation","vehicle"],char:'🚈',fitzpatrick_scale:false,category:"travel_and_places"},mountain_railway:{keywords:["transportation","vehicle"],char:'🚞',fitzpatrick_scale:false,category:"travel_and_places"},steam_locomotive:{keywords:["transportation","vehicle","train"],char:'🚂',fitzpatrick_scale:false,category:"travel_and_places"},train2:{keywords:["transportation","vehicle"],char:'🚆',fitzpatrick_scale:false,category:"travel_and_places"},metro:{keywords:["transportation","blue-square","mrt","underground","tube"],char:'🚇',fitzpatrick_scale:false,category:"travel_and_places"},tram:{keywords:["transportation","vehicle"],char:'🚊',fitzpatrick_scale:false,category:"travel_and_places"},station:{keywords:["transportation","vehicle","public"],char:'🚉',fitzpatrick_scale:false,category:"travel_and_places"},flying_saucer:{keywords:["transportation","vehicle","ufo"],char:'🛸',fitzpatrick_scale:false,category:"travel_and_places"},helicopter:{keywords:["transportation","vehicle","fly"],char:'🚁',fitzpatrick_scale:false,category:"travel_and_places"},small_airplane:{keywords:["flight","transportation","fly","vehicle"],char:'🛩',fitzpatrick_scale:false,category:"travel_and_places"},airplane:{keywords:["vehicle","transportation","flight","fly"],char:'✈️',fitzpatrick_scale:false,category:"travel_and_places"},flight_departure:{keywords:["airport","flight","landing"],char:'🛫',fitzpatrick_scale:false,category:"travel_and_places"},flight_arrival:{keywords:["airport","flight","boarding"],char:'🛬',fitzpatrick_scale:false,category:"travel_and_places"},sailboat:{keywords:["ship","summer","transportation","water","sailing"],char:'⛵',fitzpatrick_scale:false,category:"travel_and_places"},motor_boat:{keywords:["ship"],char:'🛥',fitzpatrick_scale:false,category:"travel_and_places"},speedboat:{keywords:["ship","transportation","vehicle","summer"],char:'🚤',fitzpatrick_scale:false,category:"travel_and_places"},ferry:{keywords:["boat","ship","yacht"],char:'⛴',fitzpatrick_scale:false,category:"travel_and_places"},passenger_ship:{keywords:["yacht","cruise","ferry"],char:'🛳',fitzpatrick_scale:false,category:"travel_and_places"},rocket:{keywords:["launch","ship","staffmode","NASA","outer space","outer_space","fly"],char:'🚀',fitzpatrick_scale:false,category:"travel_and_places"},artificial_satellite:{keywords:["communication","gps","orbit","spaceflight","NASA","ISS"],char:'🛰',fitzpatrick_scale:false,category:"travel_and_places"},seat:{keywords:["sit","airplane","transport","bus","flight","fly"],char:'💺',fitzpatrick_scale:false,category:"travel_and_places"},canoe:{keywords:["boat","paddle","water","ship"],char:'🛶',fitzpatrick_scale:false,category:"travel_and_places"},anchor:{keywords:["ship","ferry","sea","boat"],char:'⚓',fitzpatrick_scale:false,category:"travel_and_places"},construction:{keywords:["wip","progress","caution","warning"],char:'🚧',fitzpatrick_scale:false,category:"travel_and_places"},fuelpump:{keywords:["gas station","petroleum"],char:'⛽',fitzpatrick_scale:false,category:"travel_and_places"},busstop:{keywords:["transportation","wait"],char:'🚏',fitzpatrick_scale:false,category:"travel_and_places"},vertical_traffic_light:{keywords:["transportation","driving"],char:'🚦',fitzpatrick_scale:false,category:"travel_and_places"},traffic_light:{keywords:["transportation","signal"],char:'🚥',fitzpatrick_scale:false,category:"travel_and_places"},checkered_flag:{keywords:["contest","finishline","race","gokart"],char:'🏁',fitzpatrick_scale:false,category:"travel_and_places"},ship:{keywords:["transportation","titanic","deploy"],char:'🚢',fitzpatrick_scale:false,category:"travel_and_places"},ferris_wheel:{keywords:["photo","carnival","londoneye"],char:'🎡',fitzpatrick_scale:false,category:"travel_and_places"},roller_coaster:{keywords:["carnival","playground","photo","fun"],char:'🎢',fitzpatrick_scale:false,category:"travel_and_places"},carousel_horse:{keywords:["photo","carnival"],char:'🎠',fitzpatrick_scale:false,category:"travel_and_places"},building_construction:{keywords:["wip","working","progress"],char:'🏗',fitzpatrick_scale:false,category:"travel_and_places"},foggy:{keywords:["photo","mountain"],char:'🌁',fitzpatrick_scale:false,category:"travel_and_places"},tokyo_tower:{keywords:["photo","japanese"],char:'🗼',fitzpatrick_scale:false,category:"travel_and_places"},factory:{keywords:["building","industry","pollution","smoke"],char:'🏭',fitzpatrick_scale:false,category:"travel_and_places"},fountain:{keywords:["photo","summer","water","fresh"],char:'⛲',fitzpatrick_scale:false,category:"travel_and_places"},rice_scene:{keywords:["photo","japan","asia","tsukimi"],char:'🎑',fitzpatrick_scale:false,category:"travel_and_places"},mountain:{keywords:["photo","nature","environment"],char:'⛰',fitzpatrick_scale:false,category:"travel_and_places"},mountain_snow:{keywords:["photo","nature","environment","winter","cold"],char:'🏔',fitzpatrick_scale:false,category:"travel_and_places"},mount_fuji:{keywords:["photo","mountain","nature","japanese"],char:'🗻',fitzpatrick_scale:false,category:"travel_and_places"},volcano:{keywords:["photo","nature","disaster"],char:'🌋',fitzpatrick_scale:false,category:"travel_and_places"},japan:{keywords:["nation","country","japanese","asia"],char:'🗾',fitzpatrick_scale:false,category:"travel_and_places"},camping:{keywords:["photo","outdoors","tent"],char:'🏕',fitzpatrick_scale:false,category:"travel_and_places"},tent:{keywords:["photo","camping","outdoors"],char:'⛺',fitzpatrick_scale:false,category:"travel_and_places"},national_park:{keywords:["photo","environment","nature"],char:'🏞',fitzpatrick_scale:false,category:"travel_and_places"},motorway:{keywords:["road","cupertino","interstate","highway"],char:'🛣',fitzpatrick_scale:false,category:"travel_and_places"},railway_track:{keywords:["train","transportation"],char:'🛤',fitzpatrick_scale:false,category:"travel_and_places"},sunrise:{keywords:["morning","view","vacation","photo"],char:'🌅',fitzpatrick_scale:false,category:"travel_and_places"},sunrise_over_mountains:{keywords:["view","vacation","photo"],char:'🌄',fitzpatrick_scale:false,category:"travel_and_places"},desert:{keywords:["photo","warm","saharah"],char:'🏜',fitzpatrick_scale:false,category:"travel_and_places"},beach_umbrella:{keywords:["weather","summer","sunny","sand","mojito"],char:'🏖',fitzpatrick_scale:false,category:"travel_and_places"},desert_island:{keywords:["photo","tropical","mojito"],char:'🏝',fitzpatrick_scale:false,category:"travel_and_places"},city_sunrise:{keywords:["photo","good morning","dawn"],char:'🌇',fitzpatrick_scale:false,category:"travel_and_places"},city_sunset:{keywords:["photo","evening","sky","buildings"],char:'🌆',fitzpatrick_scale:false,category:"travel_and_places"},cityscape:{keywords:["photo","night life","urban"],char:'🏙',fitzpatrick_scale:false,category:"travel_and_places"},night_with_stars:{keywords:["evening","city","downtown"],char:'🌃',fitzpatrick_scale:false,category:"travel_and_places"},bridge_at_night:{keywords:["photo","sanfrancisco"],char:'🌉',fitzpatrick_scale:false,category:"travel_and_places"},milky_way:{keywords:["photo","space","stars"],char:'🌌',fitzpatrick_scale:false,category:"travel_and_places"},stars:{keywords:["night","photo"],char:'🌠',fitzpatrick_scale:false,category:"travel_and_places"},sparkler:{keywords:["stars","night","shine"],char:'🎇',fitzpatrick_scale:false,category:"travel_and_places"},fireworks:{keywords:["photo","festival","carnival","congratulations"],char:'🎆',fitzpatrick_scale:false,category:"travel_and_places"},rainbow:{keywords:["nature","happy","unicorn_face","photo","sky","spring"],char:'🌈',fitzpatrick_scale:false,category:"travel_and_places"},houses:{keywords:["buildings","photo"],char:'🏘',fitzpatrick_scale:false,category:"travel_and_places"},european_castle:{keywords:["building","royalty","history"],char:'🏰',fitzpatrick_scale:false,category:"travel_and_places"},japanese_castle:{keywords:["photo","building"],char:'🏯',fitzpatrick_scale:false,category:"travel_and_places"},stadium:{keywords:["photo","place","sports","concert","venue"],char:'🏟',fitzpatrick_scale:false,category:"travel_and_places"},statue_of_liberty:{keywords:["american","newyork"],char:'🗽',fitzpatrick_scale:false,category:"travel_and_places"},house:{keywords:["building","home"],char:'🏠',fitzpatrick_scale:false,category:"travel_and_places"},house_with_garden:{keywords:["home","plant","nature"],char:'🏡',fitzpatrick_scale:false,category:"travel_and_places"},derelict_house:{keywords:["abandon","evict","broken","building"],char:'🏚',fitzpatrick_scale:false,category:"travel_and_places"},office:{keywords:["building","bureau","work"],char:'🏢',fitzpatrick_scale:false,category:"travel_and_places"},department_store:{keywords:["building","shopping","mall"],char:'🏬',fitzpatrick_scale:false,category:"travel_and_places"},post_office:{keywords:["building","envelope","communication"],char:'🏣',fitzpatrick_scale:false,category:"travel_and_places"},european_post_office:{keywords:["building","email"],char:'🏤',fitzpatrick_scale:false,category:"travel_and_places"},hospital:{keywords:["building","health","surgery","doctor"],char:'🏥',fitzpatrick_scale:false,category:"travel_and_places"},bank:{keywords:["building","money","sales","cash","business","enterprise"],char:'🏦',fitzpatrick_scale:false,category:"travel_and_places"},hotel:{keywords:["building","accomodation","checkin"],char:'🏨',fitzpatrick_scale:false,category:"travel_and_places"},convenience_store:{keywords:["building","shopping","groceries"],char:'🏪',fitzpatrick_scale:false,category:"travel_and_places"},school:{keywords:["building","student","education","learn","teach"],char:'🏫',fitzpatrick_scale:false,category:"travel_and_places"},love_hotel:{keywords:["like","affection","dating"],char:'🏩',fitzpatrick_scale:false,category:"travel_and_places"},wedding:{keywords:["love","like","affection","couple","marriage","bride","groom"],char:'💒',fitzpatrick_scale:false,category:"travel_and_places"},classical_building:{keywords:["art","culture","history"],char:'🏛',fitzpatrick_scale:false,category:"travel_and_places"},church:{keywords:["building","religion","christ"],char:'⛪',fitzpatrick_scale:false,category:"travel_and_places"},mosque:{keywords:["islam","worship","minaret"],char:'🕌',fitzpatrick_scale:false,category:"travel_and_places"},synagogue:{keywords:["judaism","worship","temple","jewish"],char:'🕍',fitzpatrick_scale:false,category:"travel_and_places"},kaaba:{keywords:["mecca","mosque","islam"],char:'🕋',fitzpatrick_scale:false,category:"travel_and_places"},shinto_shrine:{keywords:["temple","japan","kyoto"],char:'⛩',fitzpatrick_scale:false,category:"travel_and_places"},watch:{keywords:["time","accessories"],char:'⌚',fitzpatrick_scale:false,category:"objects"},iphone:{keywords:["technology","apple","gadgets","dial"],char:'📱',fitzpatrick_scale:false,category:"objects"},calling:{keywords:["iphone","incoming"],char:'📲',fitzpatrick_scale:false,category:"objects"},computer:{keywords:["technology","laptop","screen","display","monitor"],char:'💻',fitzpatrick_scale:false,category:"objects"},keyboard:{keywords:["technology","computer","type","input","text"],char:'⌨',fitzpatrick_scale:false,category:"objects"},desktop_computer:{keywords:["technology","computing","screen"],char:'🖥',fitzpatrick_scale:false,category:"objects"},printer:{keywords:["paper","ink"],char:'🖨',fitzpatrick_scale:false,category:"objects"},computer_mouse:{keywords:["click"],char:'🖱',fitzpatrick_scale:false,category:"objects"},trackball:{keywords:["technology","trackpad"],char:'🖲',fitzpatrick_scale:false,category:"objects"},joystick:{keywords:["game","play"],char:'🕹',fitzpatrick_scale:false,category:"objects"},clamp:{keywords:["tool"],char:'🗜',fitzpatrick_scale:false,category:"objects"},minidisc:{keywords:["technology","record","data","disk","90s"],char:'💽',fitzpatrick_scale:false,category:"objects"},floppy_disk:{keywords:["oldschool","technology","save","90s","80s"],char:'💾',fitzpatrick_scale:false,category:"objects"},cd:{keywords:["technology","dvd","disk","disc","90s"],char:'💿',fitzpatrick_scale:false,category:"objects"},dvd:{keywords:["cd","disk","disc"],char:'📀',fitzpatrick_scale:false,category:"objects"},vhs:{keywords:["record","video","oldschool","90s","80s"],char:'📼',fitzpatrick_scale:false,category:"objects"},camera:{keywords:["gadgets","photography"],char:'📷',fitzpatrick_scale:false,category:"objects"},camera_flash:{keywords:["photography","gadgets"],char:'📸',fitzpatrick_scale:false,category:"objects"},video_camera:{keywords:["film","record"],char:'📹',fitzpatrick_scale:false,category:"objects"},movie_camera:{keywords:["film","record"],char:'🎥',fitzpatrick_scale:false,category:"objects"},film_projector:{keywords:["video","tape","record","movie"],char:'📽',fitzpatrick_scale:false,category:"objects"},film_strip:{keywords:["movie"],char:'🎞',fitzpatrick_scale:false,category:"objects"},telephone_receiver:{keywords:["technology","communication","dial"],char:'📞',fitzpatrick_scale:false,category:"objects"},phone:{keywords:["technology","communication","dial","telephone"],char:'☎️',fitzpatrick_scale:false,category:"objects"},pager:{keywords:["bbcall","oldschool","90s"],char:'📟',fitzpatrick_scale:false,category:"objects"},fax:{keywords:["communication","technology"],char:'📠',fitzpatrick_scale:false,category:"objects"},tv:{keywords:["technology","program","oldschool","show","television"],char:'📺',fitzpatrick_scale:false,category:"objects"},radio:{keywords:["communication","music","podcast","program"],char:'📻',fitzpatrick_scale:false,category:"objects"},studio_microphone:{keywords:["sing","recording","artist","talkshow"],char:'🎙',fitzpatrick_scale:false,category:"objects"},level_slider:{keywords:["scale"],char:'🎚',fitzpatrick_scale:false,category:"objects"},control_knobs:{keywords:["dial"],char:'🎛',fitzpatrick_scale:false,category:"objects"},compass:{keywords:["magnetic","navigation","orienteering"],char:'🧭',fitzpatrick_scale:false,category:"objects"},stopwatch:{keywords:["time","deadline"],char:'⏱',fitzpatrick_scale:false,category:"objects"},timer_clock:{keywords:["alarm"],char:'⏲',fitzpatrick_scale:false,category:"objects"},alarm_clock:{keywords:["time","wake"],char:'⏰',fitzpatrick_scale:false,category:"objects"},mantelpiece_clock:{keywords:["time"],char:'🕰',fitzpatrick_scale:false,category:"objects"},hourglass_flowing_sand:{keywords:["oldschool","time","countdown"],char:'⏳',fitzpatrick_scale:false,category:"objects"},hourglass:{keywords:["time","clock","oldschool","limit","exam","quiz","test"],char:'⌛',fitzpatrick_scale:false,category:"objects"},satellite:{keywords:["communication","future","radio","space"],char:'📡',fitzpatrick_scale:false,category:"objects"},battery:{keywords:["power","energy","sustain"],char:'🔋',fitzpatrick_scale:false,category:"objects"},electric_plug:{keywords:["charger","power"],char:'🔌',fitzpatrick_scale:false,category:"objects"},bulb:{keywords:["light","electricity","idea"],char:'💡',fitzpatrick_scale:false,category:"objects"},flashlight:{keywords:["dark","camping","sight","night"],char:'🔦',fitzpatrick_scale:false,category:"objects"},candle:{keywords:["fire","wax"],char:'🕯',fitzpatrick_scale:false,category:"objects"},fire_extinguisher:{keywords:["quench"],char:'🧯',fitzpatrick_scale:false,category:"objects"},wastebasket:{keywords:["bin","trash","rubbish","garbage","toss"],char:'🗑',fitzpatrick_scale:false,category:"objects"},oil_drum:{keywords:["barrell"],char:'🛢',fitzpatrick_scale:false,category:"objects"},money_with_wings:{keywords:["dollar","bills","payment","sale"],char:'💸',fitzpatrick_scale:false,category:"objects"},dollar:{keywords:["money","sales","bill","currency"],char:'💵',fitzpatrick_scale:false,category:"objects"},yen:{keywords:["money","sales","japanese","dollar","currency"],char:'💴',fitzpatrick_scale:false,category:"objects"},euro:{keywords:["money","sales","dollar","currency"],char:'💶',fitzpatrick_scale:false,category:"objects"},pound:{keywords:["british","sterling","money","sales","bills","uk","england","currency"],char:'💷',fitzpatrick_scale:false,category:"objects"},moneybag:{keywords:["dollar","payment","coins","sale"],char:'💰',fitzpatrick_scale:false,category:"objects"},credit_card:{keywords:["money","sales","dollar","bill","payment","shopping"],char:'💳',fitzpatrick_scale:false,category:"objects"},gem:{keywords:["blue","ruby","diamond","jewelry"],char:'💎',fitzpatrick_scale:false,category:"objects"},balance_scale:{keywords:["law","fairness","weight"],char:'⚖',fitzpatrick_scale:false,category:"objects"},toolbox:{keywords:["tools","diy","fix","maintainer","mechanic"],char:'🧰',fitzpatrick_scale:false,category:"objects"},wrench:{keywords:["tools","diy","ikea","fix","maintainer"],char:'🔧',fitzpatrick_scale:false,category:"objects"},hammer:{keywords:["tools","build","create"],char:'🔨',fitzpatrick_scale:false,category:"objects"},hammer_and_pick:{keywords:["tools","build","create"],char:'⚒',fitzpatrick_scale:false,category:"objects"},hammer_and_wrench:{keywords:["tools","build","create"],char:'🛠',fitzpatrick_scale:false,category:"objects"},pick:{keywords:["tools","dig"],char:'⛏',fitzpatrick_scale:false,category:"objects"},nut_and_bolt:{keywords:["handy","tools","fix"],char:'🔩',fitzpatrick_scale:false,category:"objects"},gear:{keywords:["cog"],char:'⚙',fitzpatrick_scale:false,category:"objects"},brick:{keywords:["bricks"],char:'🧱',fitzpatrick_scale:false,category:"objects"},chains:{keywords:["lock","arrest"],char:'⛓',fitzpatrick_scale:false,category:"objects"},magnet:{keywords:["attraction","magnetic"],char:'🧲',fitzpatrick_scale:false,category:"objects"},gun:{keywords:["violence","weapon","pistol","revolver"],char:'🔫',fitzpatrick_scale:false,category:"objects"},bomb:{keywords:["boom","explode","explosion","terrorism"],char:'💣',fitzpatrick_scale:false,category:"objects"},firecracker:{keywords:["dynamite","boom","explode","explosion","explosive"],char:'🧨',fitzpatrick_scale:false,category:"objects"},hocho:{keywords:["knife","blade","cutlery","kitchen","weapon"],char:'🔪',fitzpatrick_scale:false,category:"objects"},dagger:{keywords:["weapon"],char:'🗡',fitzpatrick_scale:false,category:"objects"},crossed_swords:{keywords:["weapon"],char:'⚔',fitzpatrick_scale:false,category:"objects"},shield:{keywords:["protection","security"],char:'🛡',fitzpatrick_scale:false,category:"objects"},smoking:{keywords:["kills","tobacco","cigarette","joint","smoke"],char:'🚬',fitzpatrick_scale:false,category:"objects"},skull_and_crossbones:{keywords:["poison","danger","deadly","scary","death","pirate","evil"],char:'☠',fitzpatrick_scale:false,category:"objects"},coffin:{keywords:["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],char:'⚰',fitzpatrick_scale:false,category:"objects"},funeral_urn:{keywords:["dead","die","death","rip","ashes"],char:'⚱',fitzpatrick_scale:false,category:"objects"},amphora:{keywords:["vase","jar"],char:'🏺',fitzpatrick_scale:false,category:"objects"},crystal_ball:{keywords:["disco","party","magic","circus","fortune_teller"],char:'🔮',fitzpatrick_scale:false,category:"objects"},prayer_beads:{keywords:["dhikr","religious"],char:'📿',fitzpatrick_scale:false,category:"objects"},nazar_amulet:{keywords:["bead","charm"],char:'🧿',fitzpatrick_scale:false,category:"objects"},barber:{keywords:["hair","salon","style"],char:'💈',fitzpatrick_scale:false,category:"objects"},alembic:{keywords:["distilling","science","experiment","chemistry"],char:'⚗',fitzpatrick_scale:false,category:"objects"},telescope:{keywords:["stars","space","zoom","science","astronomy"],char:'🔭',fitzpatrick_scale:false,category:"objects"},microscope:{keywords:["laboratory","experiment","zoomin","science","study"],char:'🔬',fitzpatrick_scale:false,category:"objects"},hole:{keywords:["embarrassing"],char:'🕳',fitzpatrick_scale:false,category:"objects"},pill:{keywords:["health","medicine","doctor","pharmacy","drug"],char:'💊',fitzpatrick_scale:false,category:"objects"},syringe:{keywords:["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],char:'💉',fitzpatrick_scale:false,category:"objects"},dna:{keywords:["biologist","genetics","life"],char:'🧬',fitzpatrick_scale:false,category:"objects"},microbe:{keywords:["amoeba","bacteria","germs"],char:'🦠',fitzpatrick_scale:false,category:"objects"},petri_dish:{keywords:["bacteria","biology","culture","lab"],char:'🧫',fitzpatrick_scale:false,category:"objects"},test_tube:{keywords:["chemistry","experiment","lab","science"],char:'🧪',fitzpatrick_scale:false,category:"objects"},thermometer:{keywords:["weather","temperature","hot","cold"],char:'🌡',fitzpatrick_scale:false,category:"objects"},broom:{keywords:["cleaning","sweeping","witch"],char:'🧹',fitzpatrick_scale:false,category:"objects"},basket:{keywords:["laundry"],char:'🧺',fitzpatrick_scale:false,category:"objects"},toilet_paper:{keywords:["roll"],char:'🧻',fitzpatrick_scale:false,category:"objects"},label:{keywords:["sale","tag"],char:'🏷',fitzpatrick_scale:false,category:"objects"},bookmark:{keywords:["favorite","label","save"],char:'🔖',fitzpatrick_scale:false,category:"objects"},toilet:{keywords:["restroom","wc","washroom","bathroom","potty"],char:'🚽',fitzpatrick_scale:false,category:"objects"},shower:{keywords:["clean","water","bathroom"],char:'🚿',fitzpatrick_scale:false,category:"objects"},bathtub:{keywords:["clean","shower","bathroom"],char:'🛁',fitzpatrick_scale:false,category:"objects"},soap:{keywords:["bar","bathing","cleaning","lather"],char:'🧼',fitzpatrick_scale:false,category:"objects"},sponge:{keywords:["absorbing","cleaning","porous"],char:'🧽',fitzpatrick_scale:false,category:"objects"},lotion_bottle:{keywords:["moisturizer","sunscreen"],char:'🧴',fitzpatrick_scale:false,category:"objects"},key:{keywords:["lock","door","password"],char:'🔑',fitzpatrick_scale:false,category:"objects"},old_key:{keywords:["lock","door","password"],char:'🗝',fitzpatrick_scale:false,category:"objects"},couch_and_lamp:{keywords:["read","chill"],char:'🛋',fitzpatrick_scale:false,category:"objects"},sleeping_bed:{keywords:["bed","rest"],char:'🛌',fitzpatrick_scale:true,category:"objects"},bed:{keywords:["sleep","rest"],char:'🛏',fitzpatrick_scale:false,category:"objects"},door:{keywords:["house","entry","exit"],char:'🚪',fitzpatrick_scale:false,category:"objects"},bellhop_bell:{keywords:["service"],char:'🛎',fitzpatrick_scale:false,category:"objects"},teddy_bear:{keywords:["plush","stuffed"],char:'🧸',fitzpatrick_scale:false,category:"objects"},framed_picture:{keywords:["photography"],char:'🖼',fitzpatrick_scale:false,category:"objects"},world_map:{keywords:["location","direction"],char:'🗺',fitzpatrick_scale:false,category:"objects"},parasol_on_ground:{keywords:["weather","summer"],char:'⛱',fitzpatrick_scale:false,category:"objects"},moyai:{keywords:["rock","easter island","moai"],char:'🗿',fitzpatrick_scale:false,category:"objects"},shopping:{keywords:["mall","buy","purchase"],char:'🛍',fitzpatrick_scale:false,category:"objects"},shopping_cart:{keywords:["trolley"],char:'🛒',fitzpatrick_scale:false,category:"objects"},balloon:{keywords:["party","celebration","birthday","circus"],char:'🎈',fitzpatrick_scale:false,category:"objects"},flags:{keywords:["fish","japanese","koinobori","carp","banner"],char:'🎏',fitzpatrick_scale:false,category:"objects"},ribbon:{keywords:["decoration","pink","girl","bowtie"],char:'🎀',fitzpatrick_scale:false,category:"objects"},gift:{keywords:["present","birthday","christmas","xmas"],char:'🎁',fitzpatrick_scale:false,category:"objects"},confetti_ball:{keywords:["festival","party","birthday","circus"],char:'🎊',fitzpatrick_scale:false,category:"objects"},tada:{keywords:["party","congratulations","birthday","magic","circus","celebration"],char:'🎉',fitzpatrick_scale:false,category:"objects"},dolls:{keywords:["japanese","toy","kimono"],char:'🎎',fitzpatrick_scale:false,category:"objects"},wind_chime:{keywords:["nature","ding","spring","bell"],char:'🎐',fitzpatrick_scale:false,category:"objects"},crossed_flags:{keywords:["japanese","nation","country","border"],char:'🎌',fitzpatrick_scale:false,category:"objects"},izakaya_lantern:{keywords:["light","paper","halloween","spooky"],char:'🏮',fitzpatrick_scale:false,category:"objects"},red_envelope:{keywords:["gift"],char:'🧧',fitzpatrick_scale:false,category:"objects"},email:{keywords:["letter","postal","inbox","communication"],char:'✉️',fitzpatrick_scale:false,category:"objects"},envelope_with_arrow:{keywords:["email","communication"],char:'📩',fitzpatrick_scale:false,category:"objects"},incoming_envelope:{keywords:["email","inbox"],char:'📨',fitzpatrick_scale:false,category:"objects"},"e-mail":{keywords:["communication","inbox"],char:'📧',fitzpatrick_scale:false,category:"objects"},love_letter:{keywords:["email","like","affection","envelope","valentines"],char:'💌',fitzpatrick_scale:false,category:"objects"},postbox:{keywords:["email","letter","envelope"],char:'📮',fitzpatrick_scale:false,category:"objects"},mailbox_closed:{keywords:["email","communication","inbox"],char:'📪',fitzpatrick_scale:false,category:"objects"},mailbox:{keywords:["email","inbox","communication"],char:'📫',fitzpatrick_scale:false,category:"objects"},mailbox_with_mail:{keywords:["email","inbox","communication"],char:'📬',fitzpatrick_scale:false,category:"objects"},mailbox_with_no_mail:{keywords:["email","inbox"],char:'📭',fitzpatrick_scale:false,category:"objects"},package:{keywords:["mail","gift","cardboard","box","moving"],char:'📦',fitzpatrick_scale:false,category:"objects"},postal_horn:{keywords:["instrument","music"],char:'📯',fitzpatrick_scale:false,category:"objects"},inbox_tray:{keywords:["email","documents"],char:'📥',fitzpatrick_scale:false,category:"objects"},outbox_tray:{keywords:["inbox","email"],char:'📤',fitzpatrick_scale:false,category:"objects"},scroll:{keywords:["documents","ancient","history","paper"],char:'📜',fitzpatrick_scale:false,category:"objects"},page_with_curl:{keywords:["documents","office","paper"],char:'📃',fitzpatrick_scale:false,category:"objects"},bookmark_tabs:{keywords:["favorite","save","order","tidy"],char:'📑',fitzpatrick_scale:false,category:"objects"},receipt:{keywords:["accounting","expenses"],char:'🧾',fitzpatrick_scale:false,category:"objects"},bar_chart:{keywords:["graph","presentation","stats"],char:'📊',fitzpatrick_scale:false,category:"objects"},chart_with_upwards_trend:{keywords:["graph","presentation","stats","recovery","business","economics","money","sales","good","success"],char:'📈',fitzpatrick_scale:false,category:"objects"},chart_with_downwards_trend:{keywords:["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],char:'📉',fitzpatrick_scale:false,category:"objects"},page_facing_up:{keywords:["documents","office","paper","information"],char:'📄',fitzpatrick_scale:false,category:"objects"},date:{keywords:["calendar","schedule"],char:'📅',fitzpatrick_scale:false,category:"objects"},calendar:{keywords:["schedule","date","planning"],char:'📆',fitzpatrick_scale:false,category:"objects"},spiral_calendar:{keywords:["date","schedule","planning"],char:'🗓',fitzpatrick_scale:false,category:"objects"},card_index:{keywords:["business","stationery"],char:'📇',fitzpatrick_scale:false,category:"objects"},card_file_box:{keywords:["business","stationery"],char:'🗃',fitzpatrick_scale:false,category:"objects"},ballot_box:{keywords:["election","vote"],char:'🗳',fitzpatrick_scale:false,category:"objects"},file_cabinet:{keywords:["filing","organizing"],char:'🗄',fitzpatrick_scale:false,category:"objects"},clipboard:{keywords:["stationery","documents"],char:'📋',fitzpatrick_scale:false,category:"objects"},spiral_notepad:{keywords:["memo","stationery"],char:'🗒',fitzpatrick_scale:false,category:"objects"},file_folder:{keywords:["documents","business","office"],char:'📁',fitzpatrick_scale:false,category:"objects"},open_file_folder:{keywords:["documents","load"],char:'📂',fitzpatrick_scale:false,category:"objects"},card_index_dividers:{keywords:["organizing","business","stationery"],char:'🗂',fitzpatrick_scale:false,category:"objects"},newspaper_roll:{keywords:["press","headline"],char:'🗞',fitzpatrick_scale:false,category:"objects"},newspaper:{keywords:["press","headline"],char:'📰',fitzpatrick_scale:false,category:"objects"},notebook:{keywords:["stationery","record","notes","paper","study"],char:'📓',fitzpatrick_scale:false,category:"objects"},closed_book:{keywords:["read","library","knowledge","textbook","learn"],char:'📕',fitzpatrick_scale:false,category:"objects"},green_book:{keywords:["read","library","knowledge","study"],char:'📗',fitzpatrick_scale:false,category:"objects"},blue_book:{keywords:["read","library","knowledge","learn","study"],char:'📘',fitzpatrick_scale:false,category:"objects"},orange_book:{keywords:["read","library","knowledge","textbook","study"],char:'📙',fitzpatrick_scale:false,category:"objects"},notebook_with_decorative_cover:{keywords:["classroom","notes","record","paper","study"],char:'📔',fitzpatrick_scale:false,category:"objects"},ledger:{keywords:["notes","paper"],char:'📒',fitzpatrick_scale:false,category:"objects"},books:{keywords:["literature","library","study"],char:'📚',fitzpatrick_scale:false,category:"objects"},open_book:{keywords:["book","read","library","knowledge","literature","learn","study"],char:'📖',fitzpatrick_scale:false,category:"objects"},safety_pin:{keywords:["diaper"],char:'🧷',fitzpatrick_scale:false,category:"objects"},link:{keywords:["rings","url"],char:'🔗',fitzpatrick_scale:false,category:"objects"},paperclip:{keywords:["documents","stationery"],char:'📎',fitzpatrick_scale:false,category:"objects"},paperclips:{keywords:["documents","stationery"],char:'🖇',fitzpatrick_scale:false,category:"objects"},scissors:{keywords:["stationery","cut"],char:'✂️',fitzpatrick_scale:false,category:"objects"},triangular_ruler:{keywords:["stationery","math","architect","sketch"],char:'📐',fitzpatrick_scale:false,category:"objects"},straight_ruler:{keywords:["stationery","calculate","length","math","school","drawing","architect","sketch"],char:'📏',fitzpatrick_scale:false,category:"objects"},abacus:{keywords:["calculation"],char:'🧮',fitzpatrick_scale:false,category:"objects"},pushpin:{keywords:["stationery","mark","here"],char:'📌',fitzpatrick_scale:false,category:"objects"},round_pushpin:{keywords:["stationery","location","map","here"],char:'📍',fitzpatrick_scale:false,category:"objects"},triangular_flag_on_post:{keywords:["mark","milestone","place"],char:'🚩',fitzpatrick_scale:false,category:"objects"},white_flag:{keywords:["losing","loser","lost","surrender","give up","fail"],char:'🏳',fitzpatrick_scale:false,category:"objects"},black_flag:{keywords:["pirate"],char:'🏴',fitzpatrick_scale:false,category:"objects"},rainbow_flag:{keywords:["flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"],char:'🏳️‍🌈',fitzpatrick_scale:false,category:"objects"},closed_lock_with_key:{keywords:["security","privacy"],char:'🔐',fitzpatrick_scale:false,category:"objects"},lock:{keywords:["security","password","padlock"],char:'🔒',fitzpatrick_scale:false,category:"objects"},unlock:{keywords:["privacy","security"],char:'🔓',fitzpatrick_scale:false,category:"objects"},lock_with_ink_pen:{keywords:["security","secret"],char:'🔏',fitzpatrick_scale:false,category:"objects"},pen:{keywords:["stationery","writing","write"],char:'🖊',fitzpatrick_scale:false,category:"objects"},fountain_pen:{keywords:["stationery","writing","write"],char:'🖋',fitzpatrick_scale:false,category:"objects"},black_nib:{keywords:["pen","stationery","writing","write"],char:'✒️',fitzpatrick_scale:false,category:"objects"},memo:{keywords:["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"],char:'📝',fitzpatrick_scale:false,category:"objects"},pencil2:{keywords:["stationery","write","paper","writing","school","study"],char:'✏️',fitzpatrick_scale:false,category:"objects"},crayon:{keywords:["drawing","creativity"],char:'🖍',fitzpatrick_scale:false,category:"objects"},paintbrush:{keywords:["drawing","creativity","art"],char:'🖌',fitzpatrick_scale:false,category:"objects"},mag:{keywords:["search","zoom","find","detective"],char:'🔍',fitzpatrick_scale:false,category:"objects"},mag_right:{keywords:["search","zoom","find","detective"],char:'🔎',fitzpatrick_scale:false,category:"objects"},heart:{keywords:["love","like","valentines"],char:'❤️',fitzpatrick_scale:false,category:"symbols"},orange_heart:{keywords:["love","like","affection","valentines"],char:'🧡',fitzpatrick_scale:false,category:"symbols"},yellow_heart:{keywords:["love","like","affection","valentines"],char:'💛',fitzpatrick_scale:false,category:"symbols"},green_heart:{keywords:["love","like","affection","valentines"],char:'💚',fitzpatrick_scale:false,category:"symbols"},blue_heart:{keywords:["love","like","affection","valentines"],char:'💙',fitzpatrick_scale:false,category:"symbols"},purple_heart:{keywords:["love","like","affection","valentines"],char:'💜',fitzpatrick_scale:false,category:"symbols"},black_heart:{keywords:["evil"],char:'🖤',fitzpatrick_scale:false,category:"symbols"},broken_heart:{keywords:["sad","sorry","break","heart","heartbreak"],char:'💔',fitzpatrick_scale:false,category:"symbols"},heavy_heart_exclamation:{keywords:["decoration","love"],char:'❣',fitzpatrick_scale:false,category:"symbols"},two_hearts:{keywords:["love","like","affection","valentines","heart"],char:'💕',fitzpatrick_scale:false,category:"symbols"},revolving_hearts:{keywords:["love","like","affection","valentines"],char:'💞',fitzpatrick_scale:false,category:"symbols"},heartbeat:{keywords:["love","like","affection","valentines","pink","heart"],char:'💓',fitzpatrick_scale:false,category:"symbols"},heartpulse:{keywords:["like","love","affection","valentines","pink"],char:'💗',fitzpatrick_scale:false,category:"symbols"},sparkling_heart:{keywords:["love","like","affection","valentines"],char:'💖',fitzpatrick_scale:false,category:"symbols"},cupid:{keywords:["love","like","heart","affection","valentines"],char:'💘',fitzpatrick_scale:false,category:"symbols"},gift_heart:{keywords:["love","valentines"],char:'💝',fitzpatrick_scale:false,category:"symbols"},heart_decoration:{keywords:["purple-square","love","like"],char:'💟',fitzpatrick_scale:false,category:"symbols"},peace_symbol:{keywords:["hippie"],char:'☮',fitzpatrick_scale:false,category:"symbols"},latin_cross:{keywords:["christianity"],char:'✝',fitzpatrick_scale:false,category:"symbols"},star_and_crescent:{keywords:["islam"],char:'☪',fitzpatrick_scale:false,category:"symbols"},om:{keywords:["hinduism","buddhism","sikhism","jainism"],char:'🕉',fitzpatrick_scale:false,category:"symbols"},wheel_of_dharma:{keywords:["hinduism","buddhism","sikhism","jainism"],char:'☸',fitzpatrick_scale:false,category:"symbols"},star_of_david:{keywords:["judaism"],char:'✡',fitzpatrick_scale:false,category:"symbols"},six_pointed_star:{keywords:["purple-square","religion","jewish","hexagram"],char:'🔯',fitzpatrick_scale:false,category:"symbols"},menorah:{keywords:["hanukkah","candles","jewish"],char:'🕎',fitzpatrick_scale:false,category:"symbols"},yin_yang:{keywords:["balance"],char:'☯',fitzpatrick_scale:false,category:"symbols"},orthodox_cross:{keywords:["suppedaneum","religion"],char:'☦',fitzpatrick_scale:false,category:"symbols"},place_of_worship:{keywords:["religion","church","temple","prayer"],char:'🛐',fitzpatrick_scale:false,category:"symbols"},ophiuchus:{keywords:["sign","purple-square","constellation","astrology"],char:'⛎',fitzpatrick_scale:false,category:"symbols"},aries:{keywords:["sign","purple-square","zodiac","astrology"],char:'♈',fitzpatrick_scale:false,category:"symbols"},taurus:{keywords:["purple-square","sign","zodiac","astrology"],char:'♉',fitzpatrick_scale:false,category:"symbols"},gemini:{keywords:["sign","zodiac","purple-square","astrology"],char:'♊',fitzpatrick_scale:false,category:"symbols"},cancer:{keywords:["sign","zodiac","purple-square","astrology"],char:'♋',fitzpatrick_scale:false,category:"symbols"},leo:{keywords:["sign","purple-square","zodiac","astrology"],char:'♌',fitzpatrick_scale:false,category:"symbols"},virgo:{keywords:["sign","zodiac","purple-square","astrology"],char:'♍',fitzpatrick_scale:false,category:"symbols"},libra:{keywords:["sign","purple-square","zodiac","astrology"],char:'♎',fitzpatrick_scale:false,category:"symbols"},scorpius:{keywords:["sign","zodiac","purple-square","astrology","scorpio"],char:'♏',fitzpatrick_scale:false,category:"symbols"},sagittarius:{keywords:["sign","zodiac","purple-square","astrology"],char:'♐',fitzpatrick_scale:false,category:"symbols"},capricorn:{keywords:["sign","zodiac","purple-square","astrology"],char:'♑',fitzpatrick_scale:false,category:"symbols"},aquarius:{keywords:["sign","purple-square","zodiac","astrology"],char:'♒',fitzpatrick_scale:false,category:"symbols"},pisces:{keywords:["purple-square","sign","zodiac","astrology"],char:'♓',fitzpatrick_scale:false,category:"symbols"},id:{keywords:["purple-square","words"],char:'🆔',fitzpatrick_scale:false,category:"symbols"},atom_symbol:{keywords:["science","physics","chemistry"],char:'⚛',fitzpatrick_scale:false,category:"symbols"},u7a7a:{keywords:["kanji","japanese","chinese","empty","sky","blue-square"],char:'🈳',fitzpatrick_scale:false,category:"symbols"},u5272:{keywords:["cut","divide","chinese","kanji","pink-square"],char:'🈹',fitzpatrick_scale:false,category:"symbols"},radioactive:{keywords:["nuclear","danger"],char:'☢',fitzpatrick_scale:false,category:"symbols"},biohazard:{keywords:["danger"],char:'☣',fitzpatrick_scale:false,category:"symbols"},mobile_phone_off:{keywords:["mute","orange-square","silence","quiet"],char:'📴',fitzpatrick_scale:false,category:"symbols"},vibration_mode:{keywords:["orange-square","phone"],char:'📳',fitzpatrick_scale:false,category:"symbols"},u6709:{keywords:["orange-square","chinese","have","kanji"],char:'🈶',fitzpatrick_scale:false,category:"symbols"},u7121:{keywords:["nothing","chinese","kanji","japanese","orange-square"],char:'🈚',fitzpatrick_scale:false,category:"symbols"},u7533:{keywords:["chinese","japanese","kanji","orange-square"],char:'🈸',fitzpatrick_scale:false,category:"symbols"},u55b6:{keywords:["japanese","opening hours","orange-square"],char:'🈺',fitzpatrick_scale:false,category:"symbols"},u6708:{keywords:["chinese","month","moon","japanese","orange-square","kanji"],char:'🈷️',fitzpatrick_scale:false,category:"symbols"},eight_pointed_black_star:{keywords:["orange-square","shape","polygon"],char:'✴️',fitzpatrick_scale:false,category:"symbols"},vs:{keywords:["words","orange-square"],char:'🆚',fitzpatrick_scale:false,category:"symbols"},accept:{keywords:["ok","good","chinese","kanji","agree","yes","orange-circle"],char:'🉑',fitzpatrick_scale:false,category:"symbols"},white_flower:{keywords:["japanese","spring"],char:'💮',fitzpatrick_scale:false,category:"symbols"},ideograph_advantage:{keywords:["chinese","kanji","obtain","get","circle"],char:'🉐',fitzpatrick_scale:false,category:"symbols"},secret:{keywords:["privacy","chinese","sshh","kanji","red-circle"],char:'㊙️',fitzpatrick_scale:false,category:"symbols"},congratulations:{keywords:["chinese","kanji","japanese","red-circle"],char:'㊗️',fitzpatrick_scale:false,category:"symbols"},u5408:{keywords:["japanese","chinese","join","kanji","red-square"],char:'🈴',fitzpatrick_scale:false,category:"symbols"},u6e80:{keywords:["full","chinese","japanese","red-square","kanji"],char:'🈵',fitzpatrick_scale:false,category:"symbols"},u7981:{keywords:["kanji","japanese","chinese","forbidden","limit","restricted","red-square"],char:'🈲',fitzpatrick_scale:false,category:"symbols"},a:{keywords:["red-square","alphabet","letter"],char:'🅰️',fitzpatrick_scale:false,category:"symbols"},b:{keywords:["red-square","alphabet","letter"],char:'🅱️',fitzpatrick_scale:false,category:"symbols"},ab:{keywords:["red-square","alphabet"],char:'🆎',fitzpatrick_scale:false,category:"symbols"},cl:{keywords:["alphabet","words","red-square"],char:'🆑',fitzpatrick_scale:false,category:"symbols"},o2:{keywords:["alphabet","red-square","letter"],char:'🅾️',fitzpatrick_scale:false,category:"symbols"},sos:{keywords:["help","red-square","words","emergency","911"],char:'🆘',fitzpatrick_scale:false,category:"symbols"},no_entry:{keywords:["limit","security","privacy","bad","denied","stop","circle"],char:'⛔',fitzpatrick_scale:false,category:"symbols"},name_badge:{keywords:["fire","forbid"],char:'📛',fitzpatrick_scale:false,category:"symbols"},no_entry_sign:{keywords:["forbid","stop","limit","denied","disallow","circle"],char:'🚫',fitzpatrick_scale:false,category:"symbols"},x:{keywords:["no","delete","remove","cancel","red"],char:'❌',fitzpatrick_scale:false,category:"symbols"},o:{keywords:["circle","round"],char:'⭕',fitzpatrick_scale:false,category:"symbols"},stop_sign:{keywords:["stop"],char:'🛑',fitzpatrick_scale:false,category:"symbols"},anger:{keywords:["angry","mad"],char:'💢',fitzpatrick_scale:false,category:"symbols"},hotsprings:{keywords:["bath","warm","relax"],char:'♨️',fitzpatrick_scale:false,category:"symbols"},no_pedestrians:{keywords:["rules","crossing","walking","circle"],char:'🚷',fitzpatrick_scale:false,category:"symbols"},do_not_litter:{keywords:["trash","bin","garbage","circle"],char:'🚯',fitzpatrick_scale:false,category:"symbols"},no_bicycles:{keywords:["cyclist","prohibited","circle"],char:'🚳',fitzpatrick_scale:false,category:"symbols"},"non-potable_water":{keywords:["drink","faucet","tap","circle"],char:'🚱',fitzpatrick_scale:false,category:"symbols"},underage:{keywords:["18","drink","pub","night","minor","circle"],char:'🔞',fitzpatrick_scale:false,category:"symbols"},no_mobile_phones:{keywords:["iphone","mute","circle"],char:'📵',fitzpatrick_scale:false,category:"symbols"},exclamation:{keywords:["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"],char:'❗',fitzpatrick_scale:false,category:"symbols"},grey_exclamation:{keywords:["surprise","punctuation","gray","wow","warning"],char:'❕',fitzpatrick_scale:false,category:"symbols"},question:{keywords:["doubt","confused"],char:'❓',fitzpatrick_scale:false,category:"symbols"},grey_question:{keywords:["doubts","gray","huh","confused"],char:'❔',fitzpatrick_scale:false,category:"symbols"},bangbang:{keywords:["exclamation","surprise"],char:'‼️',fitzpatrick_scale:false,category:"symbols"},interrobang:{keywords:["wat","punctuation","surprise"],char:'⁉️',fitzpatrick_scale:false,category:"symbols"},low_brightness:{keywords:["sun","afternoon","warm","summer"],char:'🔅',fitzpatrick_scale:false,category:"symbols"},high_brightness:{keywords:["sun","light"],char:'🔆',fitzpatrick_scale:false,category:"symbols"},trident:{keywords:["weapon","spear"],char:'🔱',fitzpatrick_scale:false,category:"symbols"},fleur_de_lis:{keywords:["decorative","scout"],char:'⚜',fitzpatrick_scale:false,category:"symbols"},part_alternation_mark:{keywords:["graph","presentation","stats","business","economics","bad"],char:'〽️',fitzpatrick_scale:false,category:"symbols"},warning:{keywords:["exclamation","wip","alert","error","problem","issue"],char:'⚠️',fitzpatrick_scale:false,category:"symbols"},children_crossing:{keywords:["school","warning","danger","sign","driving","yellow-diamond"],char:'🚸',fitzpatrick_scale:false,category:"symbols"},beginner:{keywords:["badge","shield"],char:'🔰',fitzpatrick_scale:false,category:"symbols"},recycle:{keywords:["arrow","environment","garbage","trash"],char:'♻️',fitzpatrick_scale:false,category:"symbols"},u6307:{keywords:["chinese","point","green-square","kanji"],char:'🈯',fitzpatrick_scale:false,category:"symbols"},chart:{keywords:["green-square","graph","presentation","stats"],char:'💹',fitzpatrick_scale:false,category:"symbols"},sparkle:{keywords:["stars","green-square","awesome","good","fireworks"],char:'❇️',fitzpatrick_scale:false,category:"symbols"},eight_spoked_asterisk:{keywords:["star","sparkle","green-square"],char:'✳️',fitzpatrick_scale:false,category:"symbols"},negative_squared_cross_mark:{keywords:["x","green-square","no","deny"],char:'❎',fitzpatrick_scale:false,category:"symbols"},white_check_mark:{keywords:["green-square","ok","agree","vote","election","answer","tick"],char:'✅',fitzpatrick_scale:false,category:"symbols"},diamond_shape_with_a_dot_inside:{keywords:["jewel","blue","gem","crystal","fancy"],char:'💠',fitzpatrick_scale:false,category:"symbols"},cyclone:{keywords:["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],char:'🌀',fitzpatrick_scale:false,category:"symbols"},loop:{keywords:["tape","cassette"],char:'➿',fitzpatrick_scale:false,category:"symbols"},globe_with_meridians:{keywords:["earth","international","world","internet","interweb","i18n"],char:'🌐',fitzpatrick_scale:false,category:"symbols"},m:{keywords:["alphabet","blue-circle","letter"],char:'Ⓜ️',fitzpatrick_scale:false,category:"symbols"},atm:{keywords:["money","sales","cash","blue-square","payment","bank"],char:'🏧',fitzpatrick_scale:false,category:"symbols"},sa:{keywords:["japanese","blue-square","katakana"],char:'🈂️',fitzpatrick_scale:false,category:"symbols"},passport_control:{keywords:["custom","blue-square"],char:'🛂',fitzpatrick_scale:false,category:"symbols"},customs:{keywords:["passport","border","blue-square"],char:'🛃',fitzpatrick_scale:false,category:"symbols"},baggage_claim:{keywords:["blue-square","airport","transport"],char:'🛄',fitzpatrick_scale:false,category:"symbols"},left_luggage:{keywords:["blue-square","travel"],char:'🛅',fitzpatrick_scale:false,category:"symbols"},wheelchair:{keywords:["blue-square","disabled","a11y","accessibility"],char:'♿',fitzpatrick_scale:false,category:"symbols"},no_smoking:{keywords:["cigarette","blue-square","smell","smoke"],char:'🚭',fitzpatrick_scale:false,category:"symbols"},wc:{keywords:["toilet","restroom","blue-square"],char:'🚾',fitzpatrick_scale:false,category:"symbols"},parking:{keywords:["cars","blue-square","alphabet","letter"],char:'🅿️',fitzpatrick_scale:false,category:"symbols"},potable_water:{keywords:["blue-square","liquid","restroom","cleaning","faucet"],char:'🚰',fitzpatrick_scale:false,category:"symbols"},mens:{keywords:["toilet","restroom","wc","blue-square","gender","male"],char:'🚹',fitzpatrick_scale:false,category:"symbols"},womens:{keywords:["purple-square","woman","female","toilet","loo","restroom","gender"],char:'🚺',fitzpatrick_scale:false,category:"symbols"},baby_symbol:{keywords:["orange-square","child"],char:'🚼',fitzpatrick_scale:false,category:"symbols"},restroom:{keywords:["blue-square","toilet","refresh","wc","gender"],char:'🚻',fitzpatrick_scale:false,category:"symbols"},put_litter_in_its_place:{keywords:["blue-square","sign","human","info"],char:'🚮',fitzpatrick_scale:false,category:"symbols"},cinema:{keywords:["blue-square","record","film","movie","curtain","stage","theater"],char:'🎦',fitzpatrick_scale:false,category:"symbols"},signal_strength:{keywords:["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"],char:'📶',fitzpatrick_scale:false,category:"symbols"},koko:{keywords:["blue-square","here","katakana","japanese","destination"],char:'🈁',fitzpatrick_scale:false,category:"symbols"},ng:{keywords:["blue-square","words","shape","icon"],char:'🆖',fitzpatrick_scale:false,category:"symbols"},ok:{keywords:["good","agree","yes","blue-square"],char:'🆗',fitzpatrick_scale:false,category:"symbols"},up:{keywords:["blue-square","above","high"],char:'🆙',fitzpatrick_scale:false,category:"symbols"},cool:{keywords:["words","blue-square"],char:'🆒',fitzpatrick_scale:false,category:"symbols"},new:{keywords:["blue-square","words","start"],char:'🆕',fitzpatrick_scale:false,category:"symbols"},free:{keywords:["blue-square","words"],char:'🆓',fitzpatrick_scale:false,category:"symbols"},zero:{keywords:["0","numbers","blue-square","null"],char:'0️⃣',fitzpatrick_scale:false,category:"symbols"},one:{keywords:["blue-square","numbers","1"],char:'1️⃣',fitzpatrick_scale:false,category:"symbols"},two:{keywords:["numbers","2","prime","blue-square"],char:'2️⃣',fitzpatrick_scale:false,category:"symbols"},three:{keywords:["3","numbers","prime","blue-square"],char:'3️⃣',fitzpatrick_scale:false,category:"symbols"},four:{keywords:["4","numbers","blue-square"],char:'4️⃣',fitzpatrick_scale:false,category:"symbols"},five:{keywords:["5","numbers","blue-square","prime"],char:'5️⃣',fitzpatrick_scale:false,category:"symbols"},six:{keywords:["6","numbers","blue-square"],char:'6️⃣',fitzpatrick_scale:false,category:"symbols"},seven:{keywords:["7","numbers","blue-square","prime"],char:'7️⃣',fitzpatrick_scale:false,category:"symbols"},eight:{keywords:["8","blue-square","numbers"],char:'8️⃣',fitzpatrick_scale:false,category:"symbols"},nine:{keywords:["blue-square","numbers","9"],char:'9️⃣',fitzpatrick_scale:false,category:"symbols"},keycap_ten:{keywords:["numbers","10","blue-square"],char:'🔟',fitzpatrick_scale:false,category:"symbols"},asterisk:{keywords:["star","keycap"],char:'*⃣',fitzpatrick_scale:false,category:"symbols"},eject_button:{keywords:["blue-square"],char:'⏏️',fitzpatrick_scale:false,category:"symbols"},arrow_forward:{keywords:["blue-square","right","direction","play"],char:'▶️',fitzpatrick_scale:false,category:"symbols"},pause_button:{keywords:["pause","blue-square"],char:'⏸',fitzpatrick_scale:false,category:"symbols"},next_track_button:{keywords:["forward","next","blue-square"],char:'⏭',fitzpatrick_scale:false,category:"symbols"},stop_button:{keywords:["blue-square"],char:'⏹',fitzpatrick_scale:false,category:"symbols"},record_button:{keywords:["blue-square"],char:'⏺',fitzpatrick_scale:false,category:"symbols"},play_or_pause_button:{keywords:["blue-square","play","pause"],char:'⏯',fitzpatrick_scale:false,category:"symbols"},previous_track_button:{keywords:["backward"],char:'⏮',fitzpatrick_scale:false,category:"symbols"},fast_forward:{keywords:["blue-square","play","speed","continue"],char:'⏩',fitzpatrick_scale:false,category:"symbols"},rewind:{keywords:["play","blue-square"],char:'⏪',fitzpatrick_scale:false,category:"symbols"},twisted_rightwards_arrows:{keywords:["blue-square","shuffle","music","random"],char:'🔀',fitzpatrick_scale:false,category:"symbols"},repeat:{keywords:["loop","record"],char:'🔁',fitzpatrick_scale:false,category:"symbols"},repeat_one:{keywords:["blue-square","loop"],char:'🔂',fitzpatrick_scale:false,category:"symbols"},arrow_backward:{keywords:["blue-square","left","direction"],char:'◀️',fitzpatrick_scale:false,category:"symbols"},arrow_up_small:{keywords:["blue-square","triangle","direction","point","forward","top"],char:'🔼',fitzpatrick_scale:false,category:"symbols"},arrow_down_small:{keywords:["blue-square","direction","bottom"],char:'🔽',fitzpatrick_scale:false,category:"symbols"},arrow_double_up:{keywords:["blue-square","direction","top"],char:'⏫',fitzpatrick_scale:false,category:"symbols"},arrow_double_down:{keywords:["blue-square","direction","bottom"],char:'⏬',fitzpatrick_scale:false,category:"symbols"},arrow_right:{keywords:["blue-square","next"],char:'➡️',fitzpatrick_scale:false,category:"symbols"},arrow_left:{keywords:["blue-square","previous","back"],char:'⬅️',fitzpatrick_scale:false,category:"symbols"},arrow_up:{keywords:["blue-square","continue","top","direction"],char:'⬆️',fitzpatrick_scale:false,category:"symbols"},arrow_down:{keywords:["blue-square","direction","bottom"],char:'⬇️',fitzpatrick_scale:false,category:"symbols"},arrow_upper_right:{keywords:["blue-square","point","direction","diagonal","northeast"],char:'↗️',fitzpatrick_scale:false,category:"symbols"},arrow_lower_right:{keywords:["blue-square","direction","diagonal","southeast"],char:'↘️',fitzpatrick_scale:false,category:"symbols"},arrow_lower_left:{keywords:["blue-square","direction","diagonal","southwest"],char:'↙️',fitzpatrick_scale:false,category:"symbols"},arrow_upper_left:{keywords:["blue-square","point","direction","diagonal","northwest"],char:'↖️',fitzpatrick_scale:false,category:"symbols"},arrow_up_down:{keywords:["blue-square","direction","way","vertical"],char:'↕️',fitzpatrick_scale:false,category:"symbols"},left_right_arrow:{keywords:["shape","direction","horizontal","sideways"],char:'↔️',fitzpatrick_scale:false,category:"symbols"},arrows_counterclockwise:{keywords:["blue-square","sync","cycle"],char:'🔄',fitzpatrick_scale:false,category:"symbols"},arrow_right_hook:{keywords:["blue-square","return","rotate","direction"],char:'↪️',fitzpatrick_scale:false,category:"symbols"},leftwards_arrow_with_hook:{keywords:["back","return","blue-square","undo","enter"],char:'↩️',fitzpatrick_scale:false,category:"symbols"},arrow_heading_up:{keywords:["blue-square","direction","top"],char:'⤴️',fitzpatrick_scale:false,category:"symbols"},arrow_heading_down:{keywords:["blue-square","direction","bottom"],char:'⤵️',fitzpatrick_scale:false,category:"symbols"},hash:{keywords:["symbol","blue-square","twitter"],char:'#️⃣',fitzpatrick_scale:false,category:"symbols"},information_source:{keywords:["blue-square","alphabet","letter"],char:'ℹ️',fitzpatrick_scale:false,category:"symbols"},abc:{keywords:["blue-square","alphabet"],char:'🔤',fitzpatrick_scale:false,category:"symbols"},abcd:{keywords:["blue-square","alphabet"],char:'🔡',fitzpatrick_scale:false,category:"symbols"},capital_abcd:{keywords:["alphabet","words","blue-square"],char:'🔠',fitzpatrick_scale:false,category:"symbols"},symbols:{keywords:["blue-square","music","note","ampersand","percent","glyphs","characters"],char:'🔣',fitzpatrick_scale:false,category:"symbols"},musical_note:{keywords:["score","tone","sound"],char:'🎵',fitzpatrick_scale:false,category:"symbols"},notes:{keywords:["music","score"],char:'🎶',fitzpatrick_scale:false,category:"symbols"},wavy_dash:{keywords:["draw","line","moustache","mustache","squiggle","scribble"],char:'〰️',fitzpatrick_scale:false,category:"symbols"},curly_loop:{keywords:["scribble","draw","shape","squiggle"],char:'➰',fitzpatrick_scale:false,category:"symbols"},heavy_check_mark:{keywords:["ok","nike","answer","yes","tick"],char:'✔️',fitzpatrick_scale:false,category:"symbols"},arrows_clockwise:{keywords:["sync","cycle","round","repeat"],char:'🔃',fitzpatrick_scale:false,category:"symbols"},heavy_plus_sign:{keywords:["math","calculation","addition","more","increase"],char:'➕',fitzpatrick_scale:false,category:"symbols"},heavy_minus_sign:{keywords:["math","calculation","subtract","less"],char:'➖',fitzpatrick_scale:false,category:"symbols"},heavy_division_sign:{keywords:["divide","math","calculation"],char:'➗',fitzpatrick_scale:false,category:"symbols"},heavy_multiplication_x:{keywords:["math","calculation"],char:'✖️',fitzpatrick_scale:false,category:"symbols"},infinity:{keywords:["forever"],char:'♾',fitzpatrick_scale:false,category:"symbols"},heavy_dollar_sign:{keywords:["money","sales","payment","currency","buck"],char:'💲',fitzpatrick_scale:false,category:"symbols"},currency_exchange:{keywords:["money","sales","dollar","travel"],char:'💱',fitzpatrick_scale:false,category:"symbols"},copyright:{keywords:["ip","license","circle","law","legal"],char:'©️',fitzpatrick_scale:false,category:"symbols"},registered:{keywords:["alphabet","circle"],char:'®️',fitzpatrick_scale:false,category:"symbols"},tm:{keywords:["trademark","brand","law","legal"],char:'™️',fitzpatrick_scale:false,category:"symbols"},end:{keywords:["words","arrow"],char:'🔚',fitzpatrick_scale:false,category:"symbols"},back:{keywords:["arrow","words","return"],char:'🔙',fitzpatrick_scale:false,category:"symbols"},on:{keywords:["arrow","words"],char:'🔛',fitzpatrick_scale:false,category:"symbols"},top:{keywords:["words","blue-square"],char:'🔝',fitzpatrick_scale:false,category:"symbols"},soon:{keywords:["arrow","words"],char:'🔜',fitzpatrick_scale:false,category:"symbols"},ballot_box_with_check:{keywords:["ok","agree","confirm","black-square","vote","election","yes","tick"],char:'☑️',fitzpatrick_scale:false,category:"symbols"},radio_button:{keywords:["input","old","music","circle"],char:'🔘',fitzpatrick_scale:false,category:"symbols"},white_circle:{keywords:["shape","round"],char:'⚪',fitzpatrick_scale:false,category:"symbols"},black_circle:{keywords:["shape","button","round"],char:'⚫',fitzpatrick_scale:false,category:"symbols"},red_circle:{keywords:["shape","error","danger"],char:'🔴',fitzpatrick_scale:false,category:"symbols"},large_blue_circle:{keywords:["shape","icon","button"],char:'🔵',fitzpatrick_scale:false,category:"symbols"},small_orange_diamond:{keywords:["shape","jewel","gem"],char:'🔸',fitzpatrick_scale:false,category:"symbols"},small_blue_diamond:{keywords:["shape","jewel","gem"],char:'🔹',fitzpatrick_scale:false,category:"symbols"},large_orange_diamond:{keywords:["shape","jewel","gem"],char:'🔶',fitzpatrick_scale:false,category:"symbols"},large_blue_diamond:{keywords:["shape","jewel","gem"],char:'🔷',fitzpatrick_scale:false,category:"symbols"},small_red_triangle:{keywords:["shape","direction","up","top"],char:'🔺',fitzpatrick_scale:false,category:"symbols"},black_small_square:{keywords:["shape","icon"],char:'▪️',fitzpatrick_scale:false,category:"symbols"},white_small_square:{keywords:["shape","icon"],char:'▫️',fitzpatrick_scale:false,category:"symbols"},black_large_square:{keywords:["shape","icon","button"],char:'⬛',fitzpatrick_scale:false,category:"symbols"},white_large_square:{keywords:["shape","icon","stone","button"],char:'⬜',fitzpatrick_scale:false,category:"symbols"},small_red_triangle_down:{keywords:["shape","direction","bottom"],char:'🔻',fitzpatrick_scale:false,category:"symbols"},black_medium_square:{keywords:["shape","button","icon"],char:'◼️',fitzpatrick_scale:false,category:"symbols"},white_medium_square:{keywords:["shape","stone","icon"],char:'◻️',fitzpatrick_scale:false,category:"symbols"},black_medium_small_square:{keywords:["icon","shape","button"],char:'◾',fitzpatrick_scale:false,category:"symbols"},white_medium_small_square:{keywords:["shape","stone","icon","button"],char:'◽',fitzpatrick_scale:false,category:"symbols"},black_square_button:{keywords:["shape","input","frame"],char:'🔲',fitzpatrick_scale:false,category:"symbols"},white_square_button:{keywords:["shape","input"],char:'🔳',fitzpatrick_scale:false,category:"symbols"},speaker:{keywords:["sound","volume","silence","broadcast"],char:'🔈',fitzpatrick_scale:false,category:"symbols"},sound:{keywords:["volume","speaker","broadcast"],char:'🔉',fitzpatrick_scale:false,category:"symbols"},loud_sound:{keywords:["volume","noise","noisy","speaker","broadcast"],char:'🔊',fitzpatrick_scale:false,category:"symbols"},mute:{keywords:["sound","volume","silence","quiet"],char:'🔇',fitzpatrick_scale:false,category:"symbols"},mega:{keywords:["sound","speaker","volume"],char:'📣',fitzpatrick_scale:false,category:"symbols"},loudspeaker:{keywords:["volume","sound"],char:'📢',fitzpatrick_scale:false,category:"symbols"},bell:{keywords:["sound","notification","christmas","xmas","chime"],char:'🔔',fitzpatrick_scale:false,category:"symbols"},no_bell:{keywords:["sound","volume","mute","quiet","silent"],char:'🔕',fitzpatrick_scale:false,category:"symbols"},black_joker:{keywords:["poker","cards","game","play","magic"],char:'🃏',fitzpatrick_scale:false,category:"symbols"},mahjong:{keywords:["game","play","chinese","kanji"],char:'🀄',fitzpatrick_scale:false,category:"symbols"},spades:{keywords:["poker","cards","suits","magic"],char:'♠️',fitzpatrick_scale:false,category:"symbols"},clubs:{keywords:["poker","cards","magic","suits"],char:'♣️',fitzpatrick_scale:false,category:"symbols"},hearts:{keywords:["poker","cards","magic","suits"],char:'♥️',fitzpatrick_scale:false,category:"symbols"},diamonds:{keywords:["poker","cards","magic","suits"],char:'♦️',fitzpatrick_scale:false,category:"symbols"},flower_playing_cards:{keywords:["game","sunset","red"],char:'🎴',fitzpatrick_scale:false,category:"symbols"},thought_balloon:{keywords:["bubble","cloud","speech","thinking","dream"],char:'💭',fitzpatrick_scale:false,category:"symbols"},right_anger_bubble:{keywords:["caption","speech","thinking","mad"],char:'🗯',fitzpatrick_scale:false,category:"symbols"},speech_balloon:{keywords:["bubble","words","message","talk","chatting"],char:'💬',fitzpatrick_scale:false,category:"symbols"},left_speech_bubble:{keywords:["words","message","talk","chatting"],char:'🗨',fitzpatrick_scale:false,category:"symbols"},clock1:{keywords:["time","late","early","schedule"],char:'🕐',fitzpatrick_scale:false,category:"symbols"},clock2:{keywords:["time","late","early","schedule"],char:'🕑',fitzpatrick_scale:false,category:"symbols"},clock3:{keywords:["time","late","early","schedule"],char:'🕒',fitzpatrick_scale:false,category:"symbols"},clock4:{keywords:["time","late","early","schedule"],char:'🕓',fitzpatrick_scale:false,category:"symbols"},clock5:{keywords:["time","late","early","schedule"],char:'🕔',fitzpatrick_scale:false,category:"symbols"},clock6:{keywords:["time","late","early","schedule","dawn","dusk"],char:'🕕',fitzpatrick_scale:false,category:"symbols"},clock7:{keywords:["time","late","early","schedule"],char:'🕖',fitzpatrick_scale:false,category:"symbols"},clock8:{keywords:["time","late","early","schedule"],char:'🕗',fitzpatrick_scale:false,category:"symbols"},clock9:{keywords:["time","late","early","schedule"],char:'🕘',fitzpatrick_scale:false,category:"symbols"},clock10:{keywords:["time","late","early","schedule"],char:'🕙',fitzpatrick_scale:false,category:"symbols"},clock11:{keywords:["time","late","early","schedule"],char:'🕚',fitzpatrick_scale:false,category:"symbols"},clock12:{keywords:["time","noon","midnight","midday","late","early","schedule"],char:'🕛',fitzpatrick_scale:false,category:"symbols"},clock130:{keywords:["time","late","early","schedule"],char:'🕜',fitzpatrick_scale:false,category:"symbols"},clock230:{keywords:["time","late","early","schedule"],char:'🕝',fitzpatrick_scale:false,category:"symbols"},clock330:{keywords:["time","late","early","schedule"],char:'🕞',fitzpatrick_scale:false,category:"symbols"},clock430:{keywords:["time","late","early","schedule"],char:'🕟',fitzpatrick_scale:false,category:"symbols"},clock530:{keywords:["time","late","early","schedule"],char:'🕠',fitzpatrick_scale:false,category:"symbols"},clock630:{keywords:["time","late","early","schedule"],char:'🕡',fitzpatrick_scale:false,category:"symbols"},clock730:{keywords:["time","late","early","schedule"],char:'🕢',fitzpatrick_scale:false,category:"symbols"},clock830:{keywords:["time","late","early","schedule"],char:'🕣',fitzpatrick_scale:false,category:"symbols"},clock930:{keywords:["time","late","early","schedule"],char:'🕤',fitzpatrick_scale:false,category:"symbols"},clock1030:{keywords:["time","late","early","schedule"],char:'🕥',fitzpatrick_scale:false,category:"symbols"},clock1130:{keywords:["time","late","early","schedule"],char:'🕦',fitzpatrick_scale:false,category:"symbols"},clock1230:{keywords:["time","late","early","schedule"],char:'🕧',fitzpatrick_scale:false,category:"symbols"},afghanistan:{keywords:["af","flag","nation","country","banner"],char:'🇦🇫',fitzpatrick_scale:false,category:"flags"},aland_islands:{keywords:["Åland","islands","flag","nation","country","banner"],char:'🇦🇽',fitzpatrick_scale:false,category:"flags"},albania:{keywords:["al","flag","nation","country","banner"],char:'🇦🇱',fitzpatrick_scale:false,category:"flags"},algeria:{keywords:["dz","flag","nation","country","banner"],char:'🇩🇿',fitzpatrick_scale:false,category:"flags"},american_samoa:{keywords:["american","ws","flag","nation","country","banner"],char:'🇦🇸',fitzpatrick_scale:false,category:"flags"},andorra:{keywords:["ad","flag","nation","country","banner"],char:'🇦🇩',fitzpatrick_scale:false,category:"flags"},angola:{keywords:["ao","flag","nation","country","banner"],char:'🇦🇴',fitzpatrick_scale:false,category:"flags"},anguilla:{keywords:["ai","flag","nation","country","banner"],char:'🇦🇮',fitzpatrick_scale:false,category:"flags"},antarctica:{keywords:["aq","flag","nation","country","banner"],char:'🇦🇶',fitzpatrick_scale:false,category:"flags"},antigua_barbuda:{keywords:["antigua","barbuda","flag","nation","country","banner"],char:'🇦🇬',fitzpatrick_scale:false,category:"flags"},argentina:{keywords:["ar","flag","nation","country","banner"],char:'🇦🇷',fitzpatrick_scale:false,category:"flags"},armenia:{keywords:["am","flag","nation","country","banner"],char:'🇦🇲',fitzpatrick_scale:false,category:"flags"},aruba:{keywords:["aw","flag","nation","country","banner"],char:'🇦🇼',fitzpatrick_scale:false,category:"flags"},australia:{keywords:["au","flag","nation","country","banner"],char:'🇦🇺',fitzpatrick_scale:false,category:"flags"},austria:{keywords:["at","flag","nation","country","banner"],char:'🇦🇹',fitzpatrick_scale:false,category:"flags"},azerbaijan:{keywords:["az","flag","nation","country","banner"],char:'🇦🇿',fitzpatrick_scale:false,category:"flags"},bahamas:{keywords:["bs","flag","nation","country","banner"],char:'🇧🇸',fitzpatrick_scale:false,category:"flags"},bahrain:{keywords:["bh","flag","nation","country","banner"],char:'🇧🇭',fitzpatrick_scale:false,category:"flags"},bangladesh:{keywords:["bd","flag","nation","country","banner"],char:'🇧🇩',fitzpatrick_scale:false,category:"flags"},barbados:{keywords:["bb","flag","nation","country","banner"],char:'🇧🇧',fitzpatrick_scale:false,category:"flags"},belarus:{keywords:["by","flag","nation","country","banner"],char:'🇧🇾',fitzpatrick_scale:false,category:"flags"},belgium:{keywords:["be","flag","nation","country","banner"],char:'🇧🇪',fitzpatrick_scale:false,category:"flags"},belize:{keywords:["bz","flag","nation","country","banner"],char:'🇧🇿',fitzpatrick_scale:false,category:"flags"},benin:{keywords:["bj","flag","nation","country","banner"],char:'🇧🇯',fitzpatrick_scale:false,category:"flags"},bermuda:{keywords:["bm","flag","nation","country","banner"],char:'🇧🇲',fitzpatrick_scale:false,category:"flags"},bhutan:{keywords:["bt","flag","nation","country","banner"],char:'🇧🇹',fitzpatrick_scale:false,category:"flags"},bolivia:{keywords:["bo","flag","nation","country","banner"],char:'🇧🇴',fitzpatrick_scale:false,category:"flags"},caribbean_netherlands:{keywords:["bonaire","flag","nation","country","banner"],char:'🇧🇶',fitzpatrick_scale:false,category:"flags"},bosnia_herzegovina:{keywords:["bosnia","herzegovina","flag","nation","country","banner"],char:'🇧🇦',fitzpatrick_scale:false,category:"flags"},botswana:{keywords:["bw","flag","nation","country","banner"],char:'🇧🇼',fitzpatrick_scale:false,category:"flags"},brazil:{keywords:["br","flag","nation","country","banner"],char:'🇧🇷',fitzpatrick_scale:false,category:"flags"},british_indian_ocean_territory:{keywords:["british","indian","ocean","territory","flag","nation","country","banner"],char:'🇮🇴',fitzpatrick_scale:false,category:"flags"},british_virgin_islands:{keywords:["british","virgin","islands","bvi","flag","nation","country","banner"],char:'🇻🇬',fitzpatrick_scale:false,category:"flags"},brunei:{keywords:["bn","darussalam","flag","nation","country","banner"],char:'🇧🇳',fitzpatrick_scale:false,category:"flags"},bulgaria:{keywords:["bg","flag","nation","country","banner"],char:'🇧🇬',fitzpatrick_scale:false,category:"flags"},burkina_faso:{keywords:["burkina","faso","flag","nation","country","banner"],char:'🇧🇫',fitzpatrick_scale:false,category:"flags"},burundi:{keywords:["bi","flag","nation","country","banner"],char:'🇧🇮',fitzpatrick_scale:false,category:"flags"},cape_verde:{keywords:["cabo","verde","flag","nation","country","banner"],char:'🇨🇻',fitzpatrick_scale:false,category:"flags"},cambodia:{keywords:["kh","flag","nation","country","banner"],char:'🇰🇭',fitzpatrick_scale:false,category:"flags"},cameroon:{keywords:["cm","flag","nation","country","banner"],char:'🇨🇲',fitzpatrick_scale:false,category:"flags"},canada:{keywords:["ca","flag","nation","country","banner"],char:'🇨🇦',fitzpatrick_scale:false,category:"flags"},canary_islands:{keywords:["canary","islands","flag","nation","country","banner"],char:'🇮🇨',fitzpatrick_scale:false,category:"flags"},cayman_islands:{keywords:["cayman","islands","flag","nation","country","banner"],char:'🇰🇾',fitzpatrick_scale:false,category:"flags"},central_african_republic:{keywords:["central","african","republic","flag","nation","country","banner"],char:'🇨🇫',fitzpatrick_scale:false,category:"flags"},chad:{keywords:["td","flag","nation","country","banner"],char:'🇹🇩',fitzpatrick_scale:false,category:"flags"},chile:{keywords:["flag","nation","country","banner"],char:'🇨🇱',fitzpatrick_scale:false,category:"flags"},cn:{keywords:["china","chinese","prc","flag","country","nation","banner"],char:'🇨🇳',fitzpatrick_scale:false,category:"flags"},christmas_island:{keywords:["christmas","island","flag","nation","country","banner"],char:'🇨🇽',fitzpatrick_scale:false,category:"flags"},cocos_islands:{keywords:["cocos","keeling","islands","flag","nation","country","banner"],char:'🇨🇨',fitzpatrick_scale:false,category:"flags"},colombia:{keywords:["co","flag","nation","country","banner"],char:'🇨🇴',fitzpatrick_scale:false,category:"flags"},comoros:{keywords:["km","flag","nation","country","banner"],char:'🇰🇲',fitzpatrick_scale:false,category:"flags"},congo_brazzaville:{keywords:["congo","flag","nation","country","banner"],char:'🇨🇬',fitzpatrick_scale:false,category:"flags"},congo_kinshasa:{keywords:["congo","democratic","republic","flag","nation","country","banner"],char:'🇨🇩',fitzpatrick_scale:false,category:"flags"},cook_islands:{keywords:["cook","islands","flag","nation","country","banner"],char:'🇨🇰',fitzpatrick_scale:false,category:"flags"},costa_rica:{keywords:["costa","rica","flag","nation","country","banner"],char:'🇨🇷',fitzpatrick_scale:false,category:"flags"},croatia:{keywords:["hr","flag","nation","country","banner"],char:'🇭🇷',fitzpatrick_scale:false,category:"flags"},cuba:{keywords:["cu","flag","nation","country","banner"],char:'🇨🇺',fitzpatrick_scale:false,category:"flags"},curacao:{keywords:["curaçao","flag","nation","country","banner"],char:'🇨🇼',fitzpatrick_scale:false,category:"flags"},cyprus:{keywords:["cy","flag","nation","country","banner"],char:'🇨🇾',fitzpatrick_scale:false,category:"flags"},czech_republic:{keywords:["cz","flag","nation","country","banner"],char:'🇨🇿',fitzpatrick_scale:false,category:"flags"},denmark:{keywords:["dk","flag","nation","country","banner"],char:'🇩🇰',fitzpatrick_scale:false,category:"flags"},djibouti:{keywords:["dj","flag","nation","country","banner"],char:'🇩🇯',fitzpatrick_scale:false,category:"flags"},dominica:{keywords:["dm","flag","nation","country","banner"],char:'🇩🇲',fitzpatrick_scale:false,category:"flags"},dominican_republic:{keywords:["dominican","republic","flag","nation","country","banner"],char:'🇩🇴',fitzpatrick_scale:false,category:"flags"},ecuador:{keywords:["ec","flag","nation","country","banner"],char:'🇪🇨',fitzpatrick_scale:false,category:"flags"},egypt:{keywords:["eg","flag","nation","country","banner"],char:'🇪🇬',fitzpatrick_scale:false,category:"flags"},el_salvador:{keywords:["el","salvador","flag","nation","country","banner"],char:'🇸🇻',fitzpatrick_scale:false,category:"flags"},equatorial_guinea:{keywords:["equatorial","gn","flag","nation","country","banner"],char:'🇬🇶',fitzpatrick_scale:false,category:"flags"},eritrea:{keywords:["er","flag","nation","country","banner"],char:'🇪🇷',fitzpatrick_scale:false,category:"flags"},estonia:{keywords:["ee","flag","nation","country","banner"],char:'🇪🇪',fitzpatrick_scale:false,category:"flags"},ethiopia:{keywords:["et","flag","nation","country","banner"],char:'🇪🇹',fitzpatrick_scale:false,category:"flags"},eu:{keywords:["european","union","flag","banner"],char:'🇪🇺',fitzpatrick_scale:false,category:"flags"},falkland_islands:{keywords:["falkland","islands","malvinas","flag","nation","country","banner"],char:'🇫🇰',fitzpatrick_scale:false,category:"flags"},faroe_islands:{keywords:["faroe","islands","flag","nation","country","banner"],char:'🇫🇴',fitzpatrick_scale:false,category:"flags"},fiji:{keywords:["fj","flag","nation","country","banner"],char:'🇫🇯',fitzpatrick_scale:false,category:"flags"},finland:{keywords:["fi","flag","nation","country","banner"],char:'🇫🇮',fitzpatrick_scale:false,category:"flags"},fr:{keywords:["banner","flag","nation","france","french","country"],char:'🇫🇷',fitzpatrick_scale:false,category:"flags"},french_guiana:{keywords:["french","guiana","flag","nation","country","banner"],char:'🇬🇫',fitzpatrick_scale:false,category:"flags"},french_polynesia:{keywords:["french","polynesia","flag","nation","country","banner"],char:'🇵🇫',fitzpatrick_scale:false,category:"flags"},french_southern_territories:{keywords:["french","southern","territories","flag","nation","country","banner"],char:'🇹🇫',fitzpatrick_scale:false,category:"flags"},gabon:{keywords:["ga","flag","nation","country","banner"],char:'🇬🇦',fitzpatrick_scale:false,category:"flags"},gambia:{keywords:["gm","flag","nation","country","banner"],char:'🇬🇲',fitzpatrick_scale:false,category:"flags"},georgia:{keywords:["ge","flag","nation","country","banner"],char:'🇬🇪',fitzpatrick_scale:false,category:"flags"},de:{keywords:["german","nation","flag","country","banner"],char:'🇩🇪',fitzpatrick_scale:false,category:"flags"},ghana:{keywords:["gh","flag","nation","country","banner"],char:'🇬🇭',fitzpatrick_scale:false,category:"flags"},gibraltar:{keywords:["gi","flag","nation","country","banner"],char:'🇬🇮',fitzpatrick_scale:false,category:"flags"},greece:{keywords:["gr","flag","nation","country","banner"],char:'🇬🇷',fitzpatrick_scale:false,category:"flags"},greenland:{keywords:["gl","flag","nation","country","banner"],char:'🇬🇱',fitzpatrick_scale:false,category:"flags"},grenada:{keywords:["gd","flag","nation","country","banner"],char:'🇬🇩',fitzpatrick_scale:false,category:"flags"},guadeloupe:{keywords:["gp","flag","nation","country","banner"],char:'🇬🇵',fitzpatrick_scale:false,category:"flags"},guam:{keywords:["gu","flag","nation","country","banner"],char:'🇬🇺',fitzpatrick_scale:false,category:"flags"},guatemala:{keywords:["gt","flag","nation","country","banner"],char:'🇬🇹',fitzpatrick_scale:false,category:"flags"},guernsey:{keywords:["gg","flag","nation","country","banner"],char:'🇬🇬',fitzpatrick_scale:false,category:"flags"},guinea:{keywords:["gn","flag","nation","country","banner"],char:'🇬🇳',fitzpatrick_scale:false,category:"flags"},guinea_bissau:{keywords:["gw","bissau","flag","nation","country","banner"],char:'🇬🇼',fitzpatrick_scale:false,category:"flags"},guyana:{keywords:["gy","flag","nation","country","banner"],char:'🇬🇾',fitzpatrick_scale:false,category:"flags"},haiti:{keywords:["ht","flag","nation","country","banner"],char:'🇭🇹',fitzpatrick_scale:false,category:"flags"},honduras:{keywords:["hn","flag","nation","country","banner"],char:'🇭🇳',fitzpatrick_scale:false,category:"flags"},hong_kong:{keywords:["hong","kong","flag","nation","country","banner"],char:'🇭🇰',fitzpatrick_scale:false,category:"flags"},hungary:{keywords:["hu","flag","nation","country","banner"],char:'🇭🇺',fitzpatrick_scale:false,category:"flags"},iceland:{keywords:["is","flag","nation","country","banner"],char:'🇮🇸',fitzpatrick_scale:false,category:"flags"},india:{keywords:["in","flag","nation","country","banner"],char:'🇮🇳',fitzpatrick_scale:false,category:"flags"},indonesia:{keywords:["flag","nation","country","banner"],char:'🇮🇩',fitzpatrick_scale:false,category:"flags"},iran:{keywords:["iran,","islamic","republic","flag","nation","country","banner"],char:'🇮🇷',fitzpatrick_scale:false,category:"flags"},iraq:{keywords:["iq","flag","nation","country","banner"],char:'🇮🇶',fitzpatrick_scale:false,category:"flags"},ireland:{keywords:["ie","flag","nation","country","banner"],char:'🇮🇪',fitzpatrick_scale:false,category:"flags"},isle_of_man:{keywords:["isle","man","flag","nation","country","banner"],char:'🇮🇲',fitzpatrick_scale:false,category:"flags"},israel:{keywords:["il","flag","nation","country","banner"],char:'🇮🇱',fitzpatrick_scale:false,category:"flags"},it:{keywords:["italy","flag","nation","country","banner"],char:'🇮🇹',fitzpatrick_scale:false,category:"flags"},cote_divoire:{keywords:["ivory","coast","flag","nation","country","banner"],char:'🇨🇮',fitzpatrick_scale:false,category:"flags"},jamaica:{keywords:["jm","flag","nation","country","banner"],char:'🇯🇲',fitzpatrick_scale:false,category:"flags"},jp:{keywords:["japanese","nation","flag","country","banner"],char:'🇯🇵',fitzpatrick_scale:false,category:"flags"},jersey:{keywords:["je","flag","nation","country","banner"],char:'🇯🇪',fitzpatrick_scale:false,category:"flags"},jordan:{keywords:["jo","flag","nation","country","banner"],char:'🇯🇴',fitzpatrick_scale:false,category:"flags"},kazakhstan:{keywords:["kz","flag","nation","country","banner"],char:'🇰🇿',fitzpatrick_scale:false,category:"flags"},kenya:{keywords:["ke","flag","nation","country","banner"],char:'🇰🇪',fitzpatrick_scale:false,category:"flags"},kiribati:{keywords:["ki","flag","nation","country","banner"],char:'🇰🇮',fitzpatrick_scale:false,category:"flags"},kosovo:{keywords:["xk","flag","nation","country","banner"],char:'🇽🇰',fitzpatrick_scale:false,category:"flags"},kuwait:{keywords:["kw","flag","nation","country","banner"],char:'🇰🇼',fitzpatrick_scale:false,category:"flags"},kyrgyzstan:{keywords:["kg","flag","nation","country","banner"],char:'🇰🇬',fitzpatrick_scale:false,category:"flags"},laos:{keywords:["lao","democratic","republic","flag","nation","country","banner"],char:'🇱🇦',fitzpatrick_scale:false,category:"flags"},latvia:{keywords:["lv","flag","nation","country","banner"],char:'🇱🇻',fitzpatrick_scale:false,category:"flags"},lebanon:{keywords:["lb","flag","nation","country","banner"],char:'🇱🇧',fitzpatrick_scale:false,category:"flags"},lesotho:{keywords:["ls","flag","nation","country","banner"],char:'🇱🇸',fitzpatrick_scale:false,category:"flags"},liberia:{keywords:["lr","flag","nation","country","banner"],char:'🇱🇷',fitzpatrick_scale:false,category:"flags"},libya:{keywords:["ly","flag","nation","country","banner"],char:'🇱🇾',fitzpatrick_scale:false,category:"flags"},liechtenstein:{keywords:["li","flag","nation","country","banner"],char:'🇱🇮',fitzpatrick_scale:false,category:"flags"},lithuania:{keywords:["lt","flag","nation","country","banner"],char:'🇱🇹',fitzpatrick_scale:false,category:"flags"},luxembourg:{keywords:["lu","flag","nation","country","banner"],char:'🇱🇺',fitzpatrick_scale:false,category:"flags"},macau:{keywords:["macao","flag","nation","country","banner"],char:'🇲🇴',fitzpatrick_scale:false,category:"flags"},macedonia:{keywords:["macedonia,","flag","nation","country","banner"],char:'🇲🇰',fitzpatrick_scale:false,category:"flags"},madagascar:{keywords:["mg","flag","nation","country","banner"],char:'🇲🇬',fitzpatrick_scale:false,category:"flags"},malawi:{keywords:["mw","flag","nation","country","banner"],char:'🇲🇼',fitzpatrick_scale:false,category:"flags"},malaysia:{keywords:["my","flag","nation","country","banner"],char:'🇲🇾',fitzpatrick_scale:false,category:"flags"},maldives:{keywords:["mv","flag","nation","country","banner"],char:'🇲🇻',fitzpatrick_scale:false,category:"flags"},mali:{keywords:["ml","flag","nation","country","banner"],char:'🇲🇱',fitzpatrick_scale:false,category:"flags"},malta:{keywords:["mt","flag","nation","country","banner"],char:'🇲🇹',fitzpatrick_scale:false,category:"flags"},marshall_islands:{keywords:["marshall","islands","flag","nation","country","banner"],char:'🇲🇭',fitzpatrick_scale:false,category:"flags"},martinique:{keywords:["mq","flag","nation","country","banner"],char:'🇲🇶',fitzpatrick_scale:false,category:"flags"},mauritania:{keywords:["mr","flag","nation","country","banner"],char:'🇲🇷',fitzpatrick_scale:false,category:"flags"},mauritius:{keywords:["mu","flag","nation","country","banner"],char:'🇲🇺',fitzpatrick_scale:false,category:"flags"},mayotte:{keywords:["yt","flag","nation","country","banner"],char:'🇾🇹',fitzpatrick_scale:false,category:"flags"},mexico:{keywords:["mx","flag","nation","country","banner"],char:'🇲🇽',fitzpatrick_scale:false,category:"flags"},micronesia:{keywords:["micronesia,","federated","states","flag","nation","country","banner"],char:'🇫🇲',fitzpatrick_scale:false,category:"flags"},moldova:{keywords:["moldova,","republic","flag","nation","country","banner"],char:'🇲🇩',fitzpatrick_scale:false,category:"flags"},monaco:{keywords:["mc","flag","nation","country","banner"],char:'🇲🇨',fitzpatrick_scale:false,category:"flags"},mongolia:{keywords:["mn","flag","nation","country","banner"],char:'🇲🇳',fitzpatrick_scale:false,category:"flags"},montenegro:{keywords:["me","flag","nation","country","banner"],char:'🇲🇪',fitzpatrick_scale:false,category:"flags"},montserrat:{keywords:["ms","flag","nation","country","banner"],char:'🇲🇸',fitzpatrick_scale:false,category:"flags"},morocco:{keywords:["ma","flag","nation","country","banner"],char:'🇲🇦',fitzpatrick_scale:false,category:"flags"},mozambique:{keywords:["mz","flag","nation","country","banner"],char:'🇲🇿',fitzpatrick_scale:false,category:"flags"},myanmar:{keywords:["mm","flag","nation","country","banner"],char:'🇲🇲',fitzpatrick_scale:false,category:"flags"},namibia:{keywords:["na","flag","nation","country","banner"],char:'🇳🇦',fitzpatrick_scale:false,category:"flags"},nauru:{keywords:["nr","flag","nation","country","banner"],char:'🇳🇷',fitzpatrick_scale:false,category:"flags"},nepal:{keywords:["np","flag","nation","country","banner"],char:'🇳🇵',fitzpatrick_scale:false,category:"flags"},netherlands:{keywords:["nl","flag","nation","country","banner"],char:'🇳🇱',fitzpatrick_scale:false,category:"flags"},new_caledonia:{keywords:["new","caledonia","flag","nation","country","banner"],char:'🇳🇨',fitzpatrick_scale:false,category:"flags"},new_zealand:{keywords:["new","zealand","flag","nation","country","banner"],char:'🇳🇿',fitzpatrick_scale:false,category:"flags"},nicaragua:{keywords:["ni","flag","nation","country","banner"],char:'🇳🇮',fitzpatrick_scale:false,category:"flags"},niger:{keywords:["ne","flag","nation","country","banner"],char:'🇳🇪',fitzpatrick_scale:false,category:"flags"},nigeria:{keywords:["flag","nation","country","banner"],char:'🇳🇬',fitzpatrick_scale:false,category:"flags"},niue:{keywords:["nu","flag","nation","country","banner"],char:'🇳🇺',fitzpatrick_scale:false,category:"flags"},norfolk_island:{keywords:["norfolk","island","flag","nation","country","banner"],char:'🇳🇫',fitzpatrick_scale:false,category:"flags"},northern_mariana_islands:{keywords:["northern","mariana","islands","flag","nation","country","banner"],char:'🇲🇵',fitzpatrick_scale:false,category:"flags"},north_korea:{keywords:["north","korea","nation","flag","country","banner"],char:'🇰🇵',fitzpatrick_scale:false,category:"flags"},norway:{keywords:["no","flag","nation","country","banner"],char:'🇳🇴',fitzpatrick_scale:false,category:"flags"},oman:{keywords:["om_symbol","flag","nation","country","banner"],char:'🇴🇲',fitzpatrick_scale:false,category:"flags"},pakistan:{keywords:["pk","flag","nation","country","banner"],char:'🇵🇰',fitzpatrick_scale:false,category:"flags"},palau:{keywords:["pw","flag","nation","country","banner"],char:'🇵🇼',fitzpatrick_scale:false,category:"flags"},palestinian_territories:{keywords:["palestine","palestinian","territories","flag","nation","country","banner"],char:'🇵🇸',fitzpatrick_scale:false,category:"flags"},panama:{keywords:["pa","flag","nation","country","banner"],char:'🇵🇦',fitzpatrick_scale:false,category:"flags"},papua_new_guinea:{keywords:["papua","new","guinea","flag","nation","country","banner"],char:'🇵🇬',fitzpatrick_scale:false,category:"flags"},paraguay:{keywords:["py","flag","nation","country","banner"],char:'🇵🇾',fitzpatrick_scale:false,category:"flags"},peru:{keywords:["pe","flag","nation","country","banner"],char:'🇵🇪',fitzpatrick_scale:false,category:"flags"},philippines:{keywords:["ph","flag","nation","country","banner"],char:'🇵🇭',fitzpatrick_scale:false,category:"flags"},pitcairn_islands:{keywords:["pitcairn","flag","nation","country","banner"],char:'🇵🇳',fitzpatrick_scale:false,category:"flags"},poland:{keywords:["pl","flag","nation","country","banner"],char:'🇵🇱',fitzpatrick_scale:false,category:"flags"},portugal:{keywords:["pt","flag","nation","country","banner"],char:'🇵🇹',fitzpatrick_scale:false,category:"flags"},puerto_rico:{keywords:["puerto","rico","flag","nation","country","banner"],char:'🇵🇷',fitzpatrick_scale:false,category:"flags"},qatar:{keywords:["qa","flag","nation","country","banner"],char:'🇶🇦',fitzpatrick_scale:false,category:"flags"},reunion:{keywords:["réunion","flag","nation","country","banner"],char:'🇷🇪',fitzpatrick_scale:false,category:"flags"},romania:{keywords:["ro","flag","nation","country","banner"],char:'🇷🇴',fitzpatrick_scale:false,category:"flags"},ru:{keywords:["russian","federation","flag","nation","country","banner"],char:'🇷🇺',fitzpatrick_scale:false,category:"flags"},rwanda:{keywords:["rw","flag","nation","country","banner"],char:'🇷🇼',fitzpatrick_scale:false,category:"flags"},st_barthelemy:{keywords:["saint","barthélemy","flag","nation","country","banner"],char:'🇧🇱',fitzpatrick_scale:false,category:"flags"},st_helena:{keywords:["saint","helena","ascension","tristan","cunha","flag","nation","country","banner"],char:'🇸🇭',fitzpatrick_scale:false,category:"flags"},st_kitts_nevis:{keywords:["saint","kitts","nevis","flag","nation","country","banner"],char:'🇰🇳',fitzpatrick_scale:false,category:"flags"},st_lucia:{keywords:["saint","lucia","flag","nation","country","banner"],char:'🇱🇨',fitzpatrick_scale:false,category:"flags"},st_pierre_miquelon:{keywords:["saint","pierre","miquelon","flag","nation","country","banner"],char:'🇵🇲',fitzpatrick_scale:false,category:"flags"},st_vincent_grenadines:{keywords:["saint","vincent","grenadines","flag","nation","country","banner"],char:'🇻🇨',fitzpatrick_scale:false,category:"flags"},samoa:{keywords:["ws","flag","nation","country","banner"],char:'🇼🇸',fitzpatrick_scale:false,category:"flags"},san_marino:{keywords:["san","marino","flag","nation","country","banner"],char:'🇸🇲',fitzpatrick_scale:false,category:"flags"},sao_tome_principe:{keywords:["sao","tome","principe","flag","nation","country","banner"],char:'🇸🇹',fitzpatrick_scale:false,category:"flags"},saudi_arabia:{keywords:["flag","nation","country","banner"],char:'🇸🇦',fitzpatrick_scale:false,category:"flags"},senegal:{keywords:["sn","flag","nation","country","banner"],char:'🇸🇳',fitzpatrick_scale:false,category:"flags"},serbia:{keywords:["rs","flag","nation","country","banner"],char:'🇷🇸',fitzpatrick_scale:false,category:"flags"},seychelles:{keywords:["sc","flag","nation","country","banner"],char:'🇸🇨',fitzpatrick_scale:false,category:"flags"},sierra_leone:{keywords:["sierra","leone","flag","nation","country","banner"],char:'🇸🇱',fitzpatrick_scale:false,category:"flags"},singapore:{keywords:["sg","flag","nation","country","banner"],char:'🇸🇬',fitzpatrick_scale:false,category:"flags"},sint_maarten:{keywords:["sint","maarten","dutch","flag","nation","country","banner"],char:'🇸🇽',fitzpatrick_scale:false,category:"flags"},slovakia:{keywords:["sk","flag","nation","country","banner"],char:'🇸🇰',fitzpatrick_scale:false,category:"flags"},slovenia:{keywords:["si","flag","nation","country","banner"],char:'🇸🇮',fitzpatrick_scale:false,category:"flags"},solomon_islands:{keywords:["solomon","islands","flag","nation","country","banner"],char:'🇸🇧',fitzpatrick_scale:false,category:"flags"},somalia:{keywords:["so","flag","nation","country","banner"],char:'🇸🇴',fitzpatrick_scale:false,category:"flags"},south_africa:{keywords:["south","africa","flag","nation","country","banner"],char:'🇿🇦',fitzpatrick_scale:false,category:"flags"},south_georgia_south_sandwich_islands:{keywords:["south","georgia","sandwich","islands","flag","nation","country","banner"],char:'🇬🇸',fitzpatrick_scale:false,category:"flags"},kr:{keywords:["south","korea","nation","flag","country","banner"],char:'🇰🇷',fitzpatrick_scale:false,category:"flags"},south_sudan:{keywords:["south","sd","flag","nation","country","banner"],char:'🇸🇸',fitzpatrick_scale:false,category:"flags"},es:{keywords:["spain","flag","nation","country","banner"],char:'🇪🇸',fitzpatrick_scale:false,category:"flags"},sri_lanka:{keywords:["sri","lanka","flag","nation","country","banner"],char:'🇱🇰',fitzpatrick_scale:false,category:"flags"},sudan:{keywords:["sd","flag","nation","country","banner"],char:'🇸🇩',fitzpatrick_scale:false,category:"flags"},suriname:{keywords:["sr","flag","nation","country","banner"],char:'🇸🇷',fitzpatrick_scale:false,category:"flags"},swaziland:{keywords:["sz","flag","nation","country","banner"],char:'🇸🇿',fitzpatrick_scale:false,category:"flags"},sweden:{keywords:["se","flag","nation","country","banner"],char:'🇸🇪',fitzpatrick_scale:false,category:"flags"},switzerland:{keywords:["ch","flag","nation","country","banner"],char:'🇨🇭',fitzpatrick_scale:false,category:"flags"},syria:{keywords:["syrian","arab","republic","flag","nation","country","banner"],char:'🇸🇾',fitzpatrick_scale:false,category:"flags"},taiwan:{keywords:["tw","flag","nation","country","banner"],char:'🇹🇼',fitzpatrick_scale:false,category:"flags"},tajikistan:{keywords:["tj","flag","nation","country","banner"],char:'🇹🇯',fitzpatrick_scale:false,category:"flags"},tanzania:{keywords:["tanzania,","united","republic","flag","nation","country","banner"],char:'🇹🇿',fitzpatrick_scale:false,category:"flags"},thailand:{keywords:["th","flag","nation","country","banner"],char:'🇹🇭',fitzpatrick_scale:false,category:"flags"},timor_leste:{keywords:["timor","leste","flag","nation","country","banner"],char:'🇹🇱',fitzpatrick_scale:false,category:"flags"},togo:{keywords:["tg","flag","nation","country","banner"],char:'🇹🇬',fitzpatrick_scale:false,category:"flags"},tokelau:{keywords:["tk","flag","nation","country","banner"],char:'🇹🇰',fitzpatrick_scale:false,category:"flags"},tonga:{keywords:["to","flag","nation","country","banner"],char:'🇹🇴',fitzpatrick_scale:false,category:"flags"},trinidad_tobago:{keywords:["trinidad","tobago","flag","nation","country","banner"],char:'🇹🇹',fitzpatrick_scale:false,category:"flags"},tunisia:{keywords:["tn","flag","nation","country","banner"],char:'🇹🇳',fitzpatrick_scale:false,category:"flags"},tr:{keywords:["turkey","flag","nation","country","banner"],char:'🇹🇷',fitzpatrick_scale:false,category:"flags"},turkmenistan:{keywords:["flag","nation","country","banner"],char:'🇹🇲',fitzpatrick_scale:false,category:"flags"},turks_caicos_islands:{keywords:["turks","caicos","islands","flag","nation","country","banner"],char:'🇹🇨',fitzpatrick_scale:false,category:"flags"},tuvalu:{keywords:["flag","nation","country","banner"],char:'🇹🇻',fitzpatrick_scale:false,category:"flags"},uganda:{keywords:["ug","flag","nation","country","banner"],char:'🇺🇬',fitzpatrick_scale:false,category:"flags"},ukraine:{keywords:["ua","flag","nation","country","banner"],char:'🇺🇦',fitzpatrick_scale:false,category:"flags"},united_arab_emirates:{keywords:["united","arab","emirates","flag","nation","country","banner"],char:'🇦🇪',fitzpatrick_scale:false,category:"flags"},uk:{keywords:["united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","UK","english","england","union jack"],char:'🇬🇧',fitzpatrick_scale:false,category:"flags"},england:{keywords:["flag","english"],char:'🏴󠁧󠁢󠁥󠁮󠁧󠁿',fitzpatrick_scale:false,category:"flags"},scotland:{keywords:["flag","scottish"],char:'🏴󠁧󠁢󠁳󠁣󠁴󠁿',fitzpatrick_scale:false,category:"flags"},wales:{keywords:["flag","welsh"],char:'🏴󠁧󠁢󠁷󠁬󠁳󠁿',fitzpatrick_scale:false,category:"flags"},us:{keywords:["united","states","america","flag","nation","country","banner"],char:'🇺🇸',fitzpatrick_scale:false,category:"flags"},us_virgin_islands:{keywords:["virgin","islands","us","flag","nation","country","banner"],char:'🇻🇮',fitzpatrick_scale:false,category:"flags"},uruguay:{keywords:["uy","flag","nation","country","banner"],char:'🇺🇾',fitzpatrick_scale:false,category:"flags"},uzbekistan:{keywords:["uz","flag","nation","country","banner"],char:'🇺🇿',fitzpatrick_scale:false,category:"flags"},vanuatu:{keywords:["vu","flag","nation","country","banner"],char:'🇻🇺',fitzpatrick_scale:false,category:"flags"},vatican_city:{keywords:["vatican","city","flag","nation","country","banner"],char:'🇻🇦',fitzpatrick_scale:false,category:"flags"},venezuela:{keywords:["ve","bolivarian","republic","flag","nation","country","banner"],char:'🇻🇪',fitzpatrick_scale:false,category:"flags"},vietnam:{keywords:["viet","nam","flag","nation","country","banner"],char:'🇻🇳',fitzpatrick_scale:false,category:"flags"},wallis_futuna:{keywords:["wallis","futuna","flag","nation","country","banner"],char:'🇼🇫',fitzpatrick_scale:false,category:"flags"},western_sahara:{keywords:["western","sahara","flag","nation","country","banner"],char:'🇪🇭',fitzpatrick_scale:false,category:"flags"},yemen:{keywords:["ye","flag","nation","country","banner"],char:'🇾🇪',fitzpatrick_scale:false,category:"flags"},zambia:{keywords:["zm","flag","nation","country","banner"],char:'🇿🇲',fitzpatrick_scale:false,category:"flags"},zimbabwe:{keywords:["zw","flag","nation","country","banner"],char:'🇿🇼',fitzpatrick_scale:false,category:"flags"},united_nations:{keywords:["un","flag","banner"],char:'🇺🇳',fitzpatrick_scale:false,category:"flags"},pirate_flag:{keywords:["skull","crossbones","flag","banner"],char:'🏴‍☠️',fitzpatrick_scale:false,category:"flags"}}); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/emoticons/js/emojiimages.min.js b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojiimages.min.js new file mode 100644 index 0000000..37f3bcf --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojiimages.min.js @@ -0,0 +1,3 @@ +// Source: npm package: emojilib +// Images provided by twemoji: https://github.com/twitter/twemoji +window.tinymce.Resource.add("tinymce.plugins.emoticons",{100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:'\u{1f4af}',fitzpatrick_scale:!1,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:'\u{1f522}',fitzpatrick_scale:!1,category:"symbols"},grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:'\u{1f600}',fitzpatrick_scale:!1,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:'\u{1f62c}',fitzpatrick_scale:!1,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:'\u{1f601}',fitzpatrick_scale:!1,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:'\u{1f602}',fitzpatrick_scale:!1,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:'\u{1f923}',fitzpatrick_scale:!1,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:'\u{1f973}',fitzpatrick_scale:!1,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:'\u{1f603}',fitzpatrick_scale:!1,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:'\u{1f604}',fitzpatrick_scale:!1,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:'\u{1f605}',fitzpatrick_scale:!1,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:'\u{1f606}',fitzpatrick_scale:!1,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:'\u{1f607}',fitzpatrick_scale:!1,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:'\u{1f609}',fitzpatrick_scale:!1,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:'\u{1f60a}',fitzpatrick_scale:!1,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:'\u{1f642}',fitzpatrick_scale:!1,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:'\u{1f643}',fitzpatrick_scale:!1,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:'\u263a\ufe0f',fitzpatrick_scale:!1,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:'\u{1f60b}',fitzpatrick_scale:!1,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:'\u{1f60c}',fitzpatrick_scale:!1,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:'\u{1f60d}',fitzpatrick_scale:!1,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:'\u{1f970}',fitzpatrick_scale:!1,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:'\u{1f618}',fitzpatrick_scale:!1,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:'\u{1f617}',fitzpatrick_scale:!1,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:'\u{1f619}',fitzpatrick_scale:!1,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:'\u{1f61a}',fitzpatrick_scale:!1,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:'\u{1f61c}',fitzpatrick_scale:!1,category:"people"},zany:{keywords:["face","goofy","crazy"],char:'\u{1f92a}',fitzpatrick_scale:!1,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:'\u{1f928}',fitzpatrick_scale:!1,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:'\u{1f9d0}',fitzpatrick_scale:!1,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:'\u{1f61d}',fitzpatrick_scale:!1,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:'\u{1f61b}',fitzpatrick_scale:!1,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:'\u{1f911}',fitzpatrick_scale:!1,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:'\u{1f913}',fitzpatrick_scale:!1,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:'\u{1f60e}',fitzpatrick_scale:!1,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:'\u{1f929}',fitzpatrick_scale:!1,category:"people"},clown_face:{keywords:["face"],char:'\u{1f921}',fitzpatrick_scale:!1,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:'\u{1f920}',fitzpatrick_scale:!1,category:"people"},hugs:{keywords:["face","smile","hug"],char:'\u{1f917}',fitzpatrick_scale:!1,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:'\u{1f60f}',fitzpatrick_scale:!1,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:'\u{1f636}',fitzpatrick_scale:!1,category:"people"},neutral_face:{keywords:["indifference","meh",":|","neutral"],char:'\u{1f610}',fitzpatrick_scale:!1,category:"people"},expressionless:{keywords:["face","indifferent","-_-","meh","deadpan"],char:'\u{1f611}',fitzpatrick_scale:!1,category:"people"},unamused:{keywords:["indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"],char:'\u{1f612}',fitzpatrick_scale:!1,category:"people"},roll_eyes:{keywords:["face","eyeroll","frustrated"],char:'\u{1f644}',fitzpatrick_scale:!1,category:"people"},thinking:{keywords:["face","hmmm","think","consider"],char:'\u{1f914}',fitzpatrick_scale:!1,category:"people"},lying_face:{keywords:["face","lie","pinocchio"],char:'\u{1f925}',fitzpatrick_scale:!1,category:"people"},hand_over_mouth:{keywords:["face","whoops","shock","surprise"],char:'\u{1f92d}',fitzpatrick_scale:!1,category:"people"},shushing:{keywords:["face","quiet","shhh"],char:'\u{1f92b}',fitzpatrick_scale:!1,category:"people"},symbols_over_mouth:{keywords:["face","swearing","cursing","cussing","profanity","expletive"],char:'\u{1f92c}',fitzpatrick_scale:!1,category:"people"},exploding_head:{keywords:["face","shocked","mind","blown"],char:'\u{1f92f}',fitzpatrick_scale:!1,category:"people"},flushed:{keywords:["face","blush","shy","flattered"],char:'\u{1f633}',fitzpatrick_scale:!1,category:"people"},disappointed:{keywords:["face","sad","upset","depressed",":("],char:'\u{1f61e}',fitzpatrick_scale:!1,category:"people"},worried:{keywords:["face","concern","nervous",":("],char:'\u{1f61f}',fitzpatrick_scale:!1,category:"people"},angry:{keywords:["mad","face","annoyed","frustrated"],char:'\u{1f620}',fitzpatrick_scale:!1,category:"people"},rage:{keywords:["angry","mad","hate","despise"],char:'\u{1f621}',fitzpatrick_scale:!1,category:"people"},pensive:{keywords:["face","sad","depressed","upset"],char:'\u{1f614}',fitzpatrick_scale:!1,category:"people"},confused:{keywords:["face","indifference","huh","weird","hmmm",":/"],char:'\u{1f615}',fitzpatrick_scale:!1,category:"people"},slightly_frowning_face:{keywords:["face","frowning","disappointed","sad","upset"],char:'\u{1f641}',fitzpatrick_scale:!1,category:"people"},frowning_face:{keywords:["face","sad","upset","frown"],char:'\u2639',fitzpatrick_scale:!1,category:"people"},persevere:{keywords:["face","sick","no","upset","oops"],char:'\u{1f623}',fitzpatrick_scale:!1,category:"people"},confounded:{keywords:["face","confused","sick","unwell","oops",":S"],char:'\u{1f616}',fitzpatrick_scale:!1,category:"people"},tired_face:{keywords:["sick","whine","upset","frustrated"],char:'\u{1f62b}',fitzpatrick_scale:!1,category:"people"},weary:{keywords:["face","tired","sleepy","sad","frustrated","upset"],char:'\u{1f629}',fitzpatrick_scale:!1,category:"people"},pleading:{keywords:["face","begging","mercy"],char:'\u{1f97a}',fitzpatrick_scale:!1,category:"people"},triumph:{keywords:["face","gas","phew","proud","pride"],char:'\u{1f624}',fitzpatrick_scale:!1,category:"people"},open_mouth:{keywords:["face","surprise","impressed","wow","whoa",":O"],char:'\u{1f62e}',fitzpatrick_scale:!1,category:"people"},scream:{keywords:["face","munch","scared","omg"],char:'\u{1f631}',fitzpatrick_scale:!1,category:"people"},fearful:{keywords:["face","scared","terrified","nervous","oops","huh"],char:'\u{1f628}',fitzpatrick_scale:!1,category:"people"},cold_sweat:{keywords:["face","nervous","sweat"],char:'\u{1f630}',fitzpatrick_scale:!1,category:"people"},hushed:{keywords:["face","woo","shh"],char:'\u{1f62f}',fitzpatrick_scale:!1,category:"people"},frowning:{keywords:["face","aw","what"],char:'\u{1f626}',fitzpatrick_scale:!1,category:"people"},anguished:{keywords:["face","stunned","nervous"],char:'\u{1f627}',fitzpatrick_scale:!1,category:"people"},cry:{keywords:["face","tears","sad","depressed","upset",":'("],char:'\u{1f622}',fitzpatrick_scale:!1,category:"people"},disappointed_relieved:{keywords:["face","phew","sweat","nervous"],char:'\u{1f625}',fitzpatrick_scale:!1,category:"people"},drooling_face:{keywords:["face"],char:'\u{1f924}',fitzpatrick_scale:!1,category:"people"},sleepy:{keywords:["face","tired","rest","nap"],char:'\u{1f62a}',fitzpatrick_scale:!1,category:"people"},sweat:{keywords:["face","hot","sad","tired","exercise"],char:'\u{1f613}',fitzpatrick_scale:!1,category:"people"},hot:{keywords:["face","feverish","heat","red","sweating"],char:'\u{1f975}',fitzpatrick_scale:!1,category:"people"},cold:{keywords:["face","blue","freezing","frozen","frostbite","icicles"],char:'\u{1f976}',fitzpatrick_scale:!1,category:"people"},sob:{keywords:["face","cry","tears","sad","upset","depressed"],char:'\u{1f62d}',fitzpatrick_scale:!1,category:"people"},dizzy_face:{keywords:["spent","unconscious","xox","dizzy"],char:'\u{1f635}',fitzpatrick_scale:!1,category:"people"},astonished:{keywords:["face","xox","surprised","poisoned"],char:'\u{1f632}',fitzpatrick_scale:!1,category:"people"},zipper_mouth_face:{keywords:["face","sealed","zipper","secret"],char:'\u{1f910}',fitzpatrick_scale:!1,category:"people"},nauseated_face:{keywords:["face","vomit","gross","green","sick","throw up","ill"],char:'\u{1f922}',fitzpatrick_scale:!1,category:"people"},sneezing_face:{keywords:["face","gesundheit","sneeze","sick","allergy"],char:'\u{1f927}',fitzpatrick_scale:!1,category:"people"},vomiting:{keywords:["face","sick"],char:'\u{1f92e}',fitzpatrick_scale:!1,category:"people"},mask:{keywords:["face","sick","ill","disease"],char:'\u{1f637}',fitzpatrick_scale:!1,category:"people"},face_with_thermometer:{keywords:["sick","temperature","thermometer","cold","fever"],char:'\u{1f912}',fitzpatrick_scale:!1,category:"people"},face_with_head_bandage:{keywords:["injured","clumsy","bandage","hurt"],char:'\u{1f915}',fitzpatrick_scale:!1,category:"people"},woozy:{keywords:["face","dizzy","intoxicated","tipsy","wavy"],char:'\u{1f974}',fitzpatrick_scale:!1,category:"people"},sleeping:{keywords:["face","tired","sleepy","night","zzz"],char:'\u{1f634}',fitzpatrick_scale:!1,category:"people"},zzz:{keywords:["sleepy","tired","dream"],char:'\u{1f4a4}',fitzpatrick_scale:!1,category:"people"},poop:{keywords:["hankey","shitface","fail","turd","shit"],char:'\u{1f4a9}',fitzpatrick_scale:!1,category:"people"},smiling_imp:{keywords:["devil","horns"],char:'\u{1f608}',fitzpatrick_scale:!1,category:"people"},imp:{keywords:["devil","angry","horns"],char:'\u{1f47f}',fitzpatrick_scale:!1,category:"people"},japanese_ogre:{keywords:["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"],char:'\u{1f479}',fitzpatrick_scale:!1,category:"people"},japanese_goblin:{keywords:["red","evil","mask","monster","scary","creepy","japanese","goblin"],char:'\u{1f47a}',fitzpatrick_scale:!1,category:"people"},skull:{keywords:["dead","skeleton","creepy","death"],char:'\u{1f480}',fitzpatrick_scale:!1,category:"people"},ghost:{keywords:["halloween","spooky","scary"],char:'\u{1f47b}',fitzpatrick_scale:!1,category:"people"},alien:{keywords:["UFO","paul","weird","outer_space"],char:'\u{1f47d}',fitzpatrick_scale:!1,category:"people"},robot:{keywords:["computer","machine","bot"],char:'\u{1f916}',fitzpatrick_scale:!1,category:"people"},smiley_cat:{keywords:["animal","cats","happy","smile"],char:'\u{1f63a}',fitzpatrick_scale:!1,category:"people"},smile_cat:{keywords:["animal","cats","smile"],char:'\u{1f638}',fitzpatrick_scale:!1,category:"people"},joy_cat:{keywords:["animal","cats","haha","happy","tears"],char:'\u{1f639}',fitzpatrick_scale:!1,category:"people"},heart_eyes_cat:{keywords:["animal","love","like","affection","cats","valentines","heart"],char:'\u{1f63b}',fitzpatrick_scale:!1,category:"people"},smirk_cat:{keywords:["animal","cats","smirk"],char:'\u{1f63c}',fitzpatrick_scale:!1,category:"people"},kissing_cat:{keywords:["animal","cats","kiss"],char:'\u{1f63d}',fitzpatrick_scale:!1,category:"people"},scream_cat:{keywords:["animal","cats","munch","scared","scream"],char:'\u{1f640}',fitzpatrick_scale:!1,category:"people"},crying_cat_face:{keywords:["animal","tears","weep","sad","cats","upset","cry"],char:'\u{1f63f}',fitzpatrick_scale:!1,category:"people"},pouting_cat:{keywords:["animal","cats"],char:'\u{1f63e}',fitzpatrick_scale:!1,category:"people"},palms_up:{keywords:["hands","gesture","cupped","prayer"],char:'\u{1f932}',fitzpatrick_scale:!0,category:"people"},raised_hands:{keywords:["gesture","hooray","yea","celebration","hands"],char:'\u{1f64c}',fitzpatrick_scale:!0,category:"people"},clap:{keywords:["hands","praise","applause","congrats","yay"],char:'\u{1f44f}',fitzpatrick_scale:!0,category:"people"},wave:{keywords:["hands","gesture","goodbye","solong","farewell","hello","hi","palm"],char:'\u{1f44b}',fitzpatrick_scale:!0,category:"people"},call_me_hand:{keywords:["hands","gesture"],char:'\u{1f919}',fitzpatrick_scale:!0,category:"people"},"+1":{keywords:["thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],char:'\u{1f44d}',fitzpatrick_scale:!0,category:"people"},"-1":{keywords:["thumbsdown","no","dislike","hand"],char:'\u{1f44e}',fitzpatrick_scale:!0,category:"people"},facepunch:{keywords:["angry","violence","fist","hit","attack","hand"],char:'\u{1f44a}',fitzpatrick_scale:!0,category:"people"},fist:{keywords:["fingers","hand","grasp"],char:'\u270a',fitzpatrick_scale:!0,category:"people"},fist_left:{keywords:["hand","fistbump"],char:'\u{1f91b}',fitzpatrick_scale:!0,category:"people"},fist_right:{keywords:["hand","fistbump"],char:'\u{1f91c}',fitzpatrick_scale:!0,category:"people"},v:{keywords:["fingers","ohyeah","hand","peace","victory","two"],char:'\u270c',fitzpatrick_scale:!0,category:"people"},ok_hand:{keywords:["fingers","limbs","perfect","ok","okay"],char:'\u{1f44c}',fitzpatrick_scale:!0,category:"people"},raised_hand:{keywords:["fingers","stop","highfive","palm","ban"],char:'\u270b',fitzpatrick_scale:!0,category:"people"},raised_back_of_hand:{keywords:["fingers","raised","backhand"],char:'\u{1f91a}',fitzpatrick_scale:!0,category:"people"},open_hands:{keywords:["fingers","butterfly","hands","open"],char:'\u{1f450}',fitzpatrick_scale:!0,category:"people"},muscle:{keywords:["arm","flex","hand","summer","strong","biceps"],char:'\u{1f4aa}',fitzpatrick_scale:!0,category:"people"},pray:{keywords:["please","hope","wish","namaste","highfive"],char:'\u{1f64f}',fitzpatrick_scale:!0,category:"people"},foot:{keywords:["kick","stomp"],char:'\u{1f9b6}',fitzpatrick_scale:!0,category:"people"},leg:{keywords:["kick","limb"],char:'\u{1f9b5}',fitzpatrick_scale:!0,category:"people"},handshake:{keywords:["agreement","shake"],char:'\u{1f91d}',fitzpatrick_scale:!1,category:"people"},point_up:{keywords:["hand","fingers","direction","up"],char:'\u261d',fitzpatrick_scale:!0,category:"people"},point_up_2:{keywords:["fingers","hand","direction","up"],char:'\u{1f446}',fitzpatrick_scale:!0,category:"people"},point_down:{keywords:["fingers","hand","direction","down"],char:'\u{1f447}',fitzpatrick_scale:!0,category:"people"},point_left:{keywords:["direction","fingers","hand","left"],char:'\u{1f448}',fitzpatrick_scale:!0,category:"people"},point_right:{keywords:["fingers","hand","direction","right"],char:'\u{1f449}',fitzpatrick_scale:!0,category:"people"},fu:{keywords:["hand","fingers","rude","middle","flipping"],char:'\u{1f595}',fitzpatrick_scale:!0,category:"people"},raised_hand_with_fingers_splayed:{keywords:["hand","fingers","palm"],char:'\u{1f590}',fitzpatrick_scale:!0,category:"people"},love_you:{keywords:["hand","fingers","gesture"],char:'\u{1f91f}',fitzpatrick_scale:!0,category:"people"},metal:{keywords:["hand","fingers","evil_eye","sign_of_horns","rock_on"],char:'\u{1f918}',fitzpatrick_scale:!0,category:"people"},crossed_fingers:{keywords:["good","lucky"],char:'\u{1f91e}',fitzpatrick_scale:!0,category:"people"},vulcan_salute:{keywords:["hand","fingers","spock","star trek"],char:'\u{1f596}',fitzpatrick_scale:!0,category:"people"},writing_hand:{keywords:["lower_left_ballpoint_pen","stationery","write","compose"],char:'\u270d',fitzpatrick_scale:!0,category:"people"},selfie:{keywords:["camera","phone"],char:'\u{1f933}',fitzpatrick_scale:!0,category:"people"},nail_care:{keywords:["beauty","manicure","finger","fashion","nail"],char:'\u{1f485}',fitzpatrick_scale:!0,category:"people"},lips:{keywords:["mouth","kiss"],char:'\u{1f444}',fitzpatrick_scale:!1,category:"people"},tooth:{keywords:["teeth","dentist"],char:'\u{1f9b7}',fitzpatrick_scale:!1,category:"people"},tongue:{keywords:["mouth","playful"],char:'\u{1f445}',fitzpatrick_scale:!1,category:"people"},ear:{keywords:["face","hear","sound","listen"],char:'\u{1f442}',fitzpatrick_scale:!0,category:"people"},nose:{keywords:["smell","sniff"],char:'\u{1f443}',fitzpatrick_scale:!0,category:"people"},eye:{keywords:["face","look","see","watch","stare"],char:'\u{1f441}',fitzpatrick_scale:!1,category:"people"},eyes:{keywords:["look","watch","stalk","peek","see"],char:'\u{1f440}',fitzpatrick_scale:!1,category:"people"},brain:{keywords:["smart","intelligent"],char:'\u{1f9e0}',fitzpatrick_scale:!1,category:"people"},bust_in_silhouette:{keywords:["user","person","human"],char:'\u{1f464}',fitzpatrick_scale:!1,category:"people"},busts_in_silhouette:{keywords:["user","person","human","group","team"],char:'\u{1f465}',fitzpatrick_scale:!1,category:"people"},speaking_head:{keywords:["user","person","human","sing","say","talk"],char:'\u{1f5e3}',fitzpatrick_scale:!1,category:"people"},baby:{keywords:["child","boy","girl","toddler"],char:'\u{1f476}',fitzpatrick_scale:!0,category:"people"},child:{keywords:["gender-neutral","young"],char:'\u{1f9d2}',fitzpatrick_scale:!0,category:"people"},boy:{keywords:["man","male","guy","teenager"],char:'\u{1f466}',fitzpatrick_scale:!0,category:"people"},girl:{keywords:["female","woman","teenager"],char:'\u{1f467}',fitzpatrick_scale:!0,category:"people"},adult:{keywords:["gender-neutral","person"],char:'\u{1f9d1}',fitzpatrick_scale:!0,category:"people"},man:{keywords:["mustache","father","dad","guy","classy","sir","moustache"],char:'\u{1f468}',fitzpatrick_scale:!0,category:"people"},woman:{keywords:["female","girls","lady"],char:'\u{1f469}',fitzpatrick_scale:!0,category:"people"},blonde_woman:{keywords:["woman","female","girl","blonde","person"],char:'\u{1f471}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},blonde_man:{keywords:["man","male","boy","blonde","guy","person"],char:'\u{1f471}',fitzpatrick_scale:!0,category:"people"},bearded_person:{keywords:["person","bewhiskered"],char:'\u{1f9d4}',fitzpatrick_scale:!0,category:"people"},older_adult:{keywords:["human","elder","senior","gender-neutral"],char:'\u{1f9d3}',fitzpatrick_scale:!0,category:"people"},older_man:{keywords:["human","male","men","old","elder","senior"],char:'\u{1f474}',fitzpatrick_scale:!0,category:"people"},older_woman:{keywords:["human","female","women","lady","old","elder","senior"],char:'\u{1f475}',fitzpatrick_scale:!0,category:"people"},man_with_gua_pi_mao:{keywords:["male","boy","chinese"],char:'\u{1f472}',fitzpatrick_scale:!0,category:"people"},woman_with_headscarf:{keywords:["female","hijab","mantilla","tichel"],char:'\u{1f9d5}',fitzpatrick_scale:!0,category:"people"},woman_with_turban:{keywords:["female","indian","hinduism","arabs","woman"],char:'\u{1f473}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},man_with_turban:{keywords:["male","indian","hinduism","arabs"],char:'\u{1f473}',fitzpatrick_scale:!0,category:"people"},policewoman:{keywords:["woman","police","law","legal","enforcement","arrest","911","female"],char:'\u{1f46e}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},policeman:{keywords:["man","police","law","legal","enforcement","arrest","911"],char:'\u{1f46e}',fitzpatrick_scale:!0,category:"people"},construction_worker_woman:{keywords:["female","human","wip","build","construction","worker","labor","woman"],char:'\u{1f477}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},construction_worker_man:{keywords:["male","human","wip","guy","build","construction","worker","labor"],char:'\u{1f477}',fitzpatrick_scale:!0,category:"people"},guardswoman:{keywords:["uk","gb","british","female","royal","woman"],char:'\u{1f482}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},guardsman:{keywords:["uk","gb","british","male","guy","royal"],char:'\u{1f482}',fitzpatrick_scale:!0,category:"people"},female_detective:{keywords:["human","spy","detective","female","woman"],char:'\u{1f575}\ufe0f\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},male_detective:{keywords:["human","spy","detective"],char:'\u{1f575}',fitzpatrick_scale:!0,category:"people"},woman_health_worker:{keywords:["doctor","nurse","therapist","healthcare","woman","human"],char:'\u{1f469}\u200d\u2695\ufe0f',fitzpatrick_scale:!0,category:"people"},man_health_worker:{keywords:["doctor","nurse","therapist","healthcare","man","human"],char:'\u{1f468}\u200d\u2695\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_farmer:{keywords:["rancher","gardener","woman","human"],char:'\u{1f469}\u200d\u{1f33e}',fitzpatrick_scale:!0,category:"people"},man_farmer:{keywords:["rancher","gardener","man","human"],char:'\u{1f468}\u200d\u{1f33e}',fitzpatrick_scale:!0,category:"people"},woman_cook:{keywords:["chef","woman","human"],char:'\u{1f469}\u200d\u{1f373}',fitzpatrick_scale:!0,category:"people"},man_cook:{keywords:["chef","man","human"],char:'\u{1f468}\u200d\u{1f373}',fitzpatrick_scale:!0,category:"people"},woman_student:{keywords:["graduate","woman","human"],char:'\u{1f469}\u200d\u{1f393}',fitzpatrick_scale:!0,category:"people"},man_student:{keywords:["graduate","man","human"],char:'\u{1f468}\u200d\u{1f393}',fitzpatrick_scale:!0,category:"people"},woman_singer:{keywords:["rockstar","entertainer","woman","human"],char:'\u{1f469}\u200d\u{1f3a4}',fitzpatrick_scale:!0,category:"people"},man_singer:{keywords:["rockstar","entertainer","man","human"],char:'\u{1f468}\u200d\u{1f3a4}',fitzpatrick_scale:!0,category:"people"},woman_teacher:{keywords:["instructor","professor","woman","human"],char:'\u{1f469}\u200d\u{1f3eb}',fitzpatrick_scale:!0,category:"people"},man_teacher:{keywords:["instructor","professor","man","human"],char:'\u{1f468}\u200d\u{1f3eb}',fitzpatrick_scale:!0,category:"people"},woman_factory_worker:{keywords:["assembly","industrial","woman","human"],char:'\u{1f469}\u200d\u{1f3ed}',fitzpatrick_scale:!0,category:"people"},man_factory_worker:{keywords:["assembly","industrial","man","human"],char:'\u{1f468}\u200d\u{1f3ed}',fitzpatrick_scale:!0,category:"people"},woman_technologist:{keywords:["coder","developer","engineer","programmer","software","woman","human","laptop","computer"],char:'\u{1f469}\u200d\u{1f4bb}',fitzpatrick_scale:!0,category:"people"},man_technologist:{keywords:["coder","developer","engineer","programmer","software","man","human","laptop","computer"],char:'\u{1f468}\u200d\u{1f4bb}',fitzpatrick_scale:!0,category:"people"},woman_office_worker:{keywords:["business","manager","woman","human"],char:'\u{1f469}\u200d\u{1f4bc}',fitzpatrick_scale:!0,category:"people"},man_office_worker:{keywords:["business","manager","man","human"],char:'\u{1f468}\u200d\u{1f4bc}',fitzpatrick_scale:!0,category:"people"},woman_mechanic:{keywords:["plumber","woman","human","wrench"],char:'\u{1f469}\u200d\u{1f527}',fitzpatrick_scale:!0,category:"people"},man_mechanic:{keywords:["plumber","man","human","wrench"],char:'\u{1f468}\u200d\u{1f527}',fitzpatrick_scale:!0,category:"people"},woman_scientist:{keywords:["biologist","chemist","engineer","physicist","woman","human"],char:'\u{1f469}\u200d\u{1f52c}',fitzpatrick_scale:!0,category:"people"},man_scientist:{keywords:["biologist","chemist","engineer","physicist","man","human"],char:'\u{1f468}\u200d\u{1f52c}',fitzpatrick_scale:!0,category:"people"},woman_artist:{keywords:["painter","woman","human"],char:'\u{1f469}\u200d\u{1f3a8}',fitzpatrick_scale:!0,category:"people"},man_artist:{keywords:["painter","man","human"],char:'\u{1f468}\u200d\u{1f3a8}',fitzpatrick_scale:!0,category:"people"},woman_firefighter:{keywords:["fireman","woman","human"],char:'\u{1f469}\u200d\u{1f692}',fitzpatrick_scale:!0,category:"people"},man_firefighter:{keywords:["fireman","man","human"],char:'\u{1f468}\u200d\u{1f692}',fitzpatrick_scale:!0,category:"people"},woman_pilot:{keywords:["aviator","plane","woman","human"],char:'\u{1f469}\u200d\u2708\ufe0f',fitzpatrick_scale:!0,category:"people"},man_pilot:{keywords:["aviator","plane","man","human"],char:'\u{1f468}\u200d\u2708\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_astronaut:{keywords:["space","rocket","woman","human"],char:'\u{1f469}\u200d\u{1f680}',fitzpatrick_scale:!0,category:"people"},man_astronaut:{keywords:["space","rocket","man","human"],char:'\u{1f468}\u200d\u{1f680}',fitzpatrick_scale:!0,category:"people"},woman_judge:{keywords:["justice","court","woman","human"],char:'\u{1f469}\u200d\u2696\ufe0f',fitzpatrick_scale:!0,category:"people"},man_judge:{keywords:["justice","court","man","human"],char:'\u{1f468}\u200d\u2696\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_superhero:{keywords:["woman","female","good","heroine","superpowers"],char:'\u{1f9b8}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},man_superhero:{keywords:["man","male","good","hero","superpowers"],char:'\u{1f9b8}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_supervillain:{keywords:["woman","female","evil","bad","criminal","heroine","superpowers"],char:'\u{1f9b9}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},man_supervillain:{keywords:["man","male","evil","bad","criminal","hero","superpowers"],char:'\u{1f9b9}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},mrs_claus:{keywords:["woman","female","xmas","mother christmas"],char:'\u{1f936}',fitzpatrick_scale:!0,category:"people"},santa:{keywords:["festival","man","male","xmas","father christmas"],char:'\u{1f385}',fitzpatrick_scale:!0,category:"people"},sorceress:{keywords:["woman","female","mage","witch"],char:'\u{1f9d9}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},wizard:{keywords:["man","male","mage","sorcerer"],char:'\u{1f9d9}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_elf:{keywords:["woman","female"],char:'\u{1f9dd}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},man_elf:{keywords:["man","male"],char:'\u{1f9dd}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_vampire:{keywords:["woman","female"],char:'\u{1f9db}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},man_vampire:{keywords:["man","male","dracula"],char:'\u{1f9db}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_zombie:{keywords:["woman","female","undead","walking dead"],char:'\u{1f9df}\u200d\u2640\ufe0f',fitzpatrick_scale:!1,category:"people"},man_zombie:{keywords:["man","male","dracula","undead","walking dead"],char:'\u{1f9df}\u200d\u2642\ufe0f',fitzpatrick_scale:!1,category:"people"},woman_genie:{keywords:["woman","female"],char:'\u{1f9de}\u200d\u2640\ufe0f',fitzpatrick_scale:!1,category:"people"},man_genie:{keywords:["man","male"],char:'\u{1f9de}\u200d\u2642\ufe0f',fitzpatrick_scale:!1,category:"people"},mermaid:{keywords:["woman","female","merwoman","ariel"],char:'\u{1f9dc}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},merman:{keywords:["man","male","triton"],char:'\u{1f9dc}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_fairy:{keywords:["woman","female"],char:'\u{1f9da}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},man_fairy:{keywords:["man","male"],char:'\u{1f9da}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},angel:{keywords:["heaven","wings","halo"],char:'\u{1f47c}',fitzpatrick_scale:!0,category:"people"},pregnant_woman:{keywords:["baby"],char:'\u{1f930}',fitzpatrick_scale:!0,category:"people"},breastfeeding:{keywords:["nursing","baby"],char:'\u{1f931}',fitzpatrick_scale:!0,category:"people"},princess:{keywords:["girl","woman","female","blond","crown","royal","queen"],char:'\u{1f478}',fitzpatrick_scale:!0,category:"people"},prince:{keywords:["boy","man","male","crown","royal","king"],char:'\u{1f934}',fitzpatrick_scale:!0,category:"people"},bride_with_veil:{keywords:["couple","marriage","wedding","woman","bride"],char:'\u{1f470}',fitzpatrick_scale:!0,category:"people"},man_in_tuxedo:{keywords:["couple","marriage","wedding","groom"],char:'\u{1f935}',fitzpatrick_scale:!0,category:"people"},running_woman:{keywords:["woman","walking","exercise","race","running","female"],char:'\u{1f3c3}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},running_man:{keywords:["man","walking","exercise","race","running"],char:'\u{1f3c3}',fitzpatrick_scale:!0,category:"people"},walking_woman:{keywords:["human","feet","steps","woman","female"],char:'\u{1f6b6}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},walking_man:{keywords:["human","feet","steps"],char:'\u{1f6b6}',fitzpatrick_scale:!0,category:"people"},dancer:{keywords:["female","girl","woman","fun"],char:'\u{1f483}',fitzpatrick_scale:!0,category:"people"},man_dancing:{keywords:["male","boy","fun","dancer"],char:'\u{1f57a}',fitzpatrick_scale:!0,category:"people"},dancing_women:{keywords:["female","bunny","women","girls"],char:'\u{1f46f}',fitzpatrick_scale:!1,category:"people"},dancing_men:{keywords:["male","bunny","men","boys"],char:'\u{1f46f}\u200d\u2642\ufe0f',fitzpatrick_scale:!1,category:"people"},couple:{keywords:["pair","people","human","love","date","dating","like","affection","valentines","marriage"],char:'\u{1f46b}',fitzpatrick_scale:!1,category:"people"},two_men_holding_hands:{keywords:["pair","couple","love","like","bromance","friendship","people","human"],char:'\u{1f46c}',fitzpatrick_scale:!1,category:"people"},two_women_holding_hands:{keywords:["pair","friendship","couple","love","like","female","people","human"],char:'\u{1f46d}',fitzpatrick_scale:!1,category:"people"},bowing_woman:{keywords:["woman","female","girl"],char:'\u{1f647}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},bowing_man:{keywords:["man","male","boy"],char:'\u{1f647}',fitzpatrick_scale:!0,category:"people"},man_facepalming:{keywords:["man","male","boy","disbelief"],char:'\u{1f926}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_facepalming:{keywords:["woman","female","girl","disbelief"],char:'\u{1f926}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_shrugging:{keywords:["woman","female","girl","confused","indifferent","doubt"],char:'\u{1f937}',fitzpatrick_scale:!0,category:"people"},man_shrugging:{keywords:["man","male","boy","confused","indifferent","doubt"],char:'\u{1f937}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},tipping_hand_woman:{keywords:["female","girl","woman","human","information"],char:'\u{1f481}',fitzpatrick_scale:!0,category:"people"},tipping_hand_man:{keywords:["male","boy","man","human","information"],char:'\u{1f481}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},no_good_woman:{keywords:["female","girl","woman","nope"],char:'\u{1f645}',fitzpatrick_scale:!0,category:"people"},no_good_man:{keywords:["male","boy","man","nope"],char:'\u{1f645}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},ok_woman:{keywords:["women","girl","female","pink","human","woman"],char:'\u{1f646}',fitzpatrick_scale:!0,category:"people"},ok_man:{keywords:["men","boy","male","blue","human","man"],char:'\u{1f646}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},raising_hand_woman:{keywords:["female","girl","woman"],char:'\u{1f64b}',fitzpatrick_scale:!0,category:"people"},raising_hand_man:{keywords:["male","boy","man"],char:'\u{1f64b}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},pouting_woman:{keywords:["female","girl","woman"],char:'\u{1f64e}',fitzpatrick_scale:!0,category:"people"},pouting_man:{keywords:["male","boy","man"],char:'\u{1f64e}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},frowning_woman:{keywords:["female","girl","woman","sad","depressed","discouraged","unhappy"],char:'\u{1f64d}',fitzpatrick_scale:!0,category:"people"},frowning_man:{keywords:["male","boy","man","sad","depressed","discouraged","unhappy"],char:'\u{1f64d}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},haircut_woman:{keywords:["female","girl","woman"],char:'\u{1f487}',fitzpatrick_scale:!0,category:"people"},haircut_man:{keywords:["male","boy","man"],char:'\u{1f487}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},massage_woman:{keywords:["female","girl","woman","head"],char:'\u{1f486}',fitzpatrick_scale:!0,category:"people"},massage_man:{keywords:["male","boy","man","head"],char:'\u{1f486}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},woman_in_steamy_room:{keywords:["female","woman","spa","steamroom","sauna"],char:'\u{1f9d6}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"people"},man_in_steamy_room:{keywords:["male","man","spa","steamroom","sauna"],char:'\u{1f9d6}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"people"},couple_with_heart_woman_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'\u{1f491}',fitzpatrick_scale:!1,category:"people"},couple_with_heart_woman_woman:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'\u{1f469}\u200d\u2764\ufe0f\u200d\u{1f469}',fitzpatrick_scale:!1,category:"people"},couple_with_heart_man_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:'\u{1f468}\u200d\u2764\ufe0f\u200d\u{1f468}',fitzpatrick_scale:!1,category:"people"},couplekiss_man_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:'\u{1f48f}',fitzpatrick_scale:!1,category:"people"},couplekiss_woman_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:'\u{1f469}\u200d\u2764\ufe0f\u200d\u{1f48b}\u200d\u{1f469}',fitzpatrick_scale:!1,category:"people"},couplekiss_man_man:{keywords:["pair","valentines","love","like","dating","marriage"],char:'\u{1f468}\u200d\u2764\ufe0f\u200d\u{1f48b}\u200d\u{1f468}',fitzpatrick_scale:!1,category:"people"},family_man_woman_boy:{keywords:["home","parents","child","mom","dad","father","mother","people","human"],char:'\u{1f46a}',fitzpatrick_scale:!1,category:"people"},family_man_woman_girl:{keywords:["home","parents","people","human","child"],char:'\u{1f468}\u200d\u{1f469}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_man_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f469}\u200d\u{1f466}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_woman_woman_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f469}\u200d\u{1f469}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_woman_woman_girl:{keywords:["home","parents","people","human","children"],char:'\u{1f469}\u200d\u{1f469}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_woman_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f469}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_woman_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f469}\u200d\u{1f469}\u200d\u{1f466}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_woman_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:'\u{1f469}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_man_man_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f468}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_man_girl:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f468}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_man_man_girl_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f468}\u200d\u{1f467}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_man_boy_boy:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f468}\u200d\u{1f466}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_man_girl_girl:{keywords:["home","parents","people","human","children"],char:'\u{1f468}\u200d\u{1f468}\u200d\u{1f467}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_woman_boy:{keywords:["home","parent","people","human","child"],char:'\u{1f469}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_woman_girl:{keywords:["home","parent","people","human","child"],char:'\u{1f469}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_woman_girl_boy:{keywords:["home","parent","people","human","children"],char:'\u{1f469}\u200d\u{1f467}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_woman_boy_boy:{keywords:["home","parent","people","human","children"],char:'\u{1f469}\u200d\u{1f466}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_woman_girl_girl:{keywords:["home","parent","people","human","children"],char:'\u{1f469}\u200d\u{1f467}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_man_boy:{keywords:["home","parent","people","human","child"],char:'\u{1f468}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_girl:{keywords:["home","parent","people","human","child"],char:'\u{1f468}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},family_man_girl_boy:{keywords:["home","parent","people","human","children"],char:'\u{1f468}\u200d\u{1f467}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_boy_boy:{keywords:["home","parent","people","human","children"],char:'\u{1f468}\u200d\u{1f466}\u200d\u{1f466}',fitzpatrick_scale:!1,category:"people"},family_man_girl_girl:{keywords:["home","parent","people","human","children"],char:'\u{1f468}\u200d\u{1f467}\u200d\u{1f467}',fitzpatrick_scale:!1,category:"people"},yarn:{keywords:["ball","crochet","knit"],char:'\u{1f9f6}',fitzpatrick_scale:!1,category:"people"},thread:{keywords:["needle","sewing","spool","string"],char:'\u{1f9f5}',fitzpatrick_scale:!1,category:"people"},coat:{keywords:["jacket"],char:'\u{1f9e5}',fitzpatrick_scale:!1,category:"people"},labcoat:{keywords:["doctor","experiment","scientist","chemist"],char:'\u{1f97c}',fitzpatrick_scale:!1,category:"people"},womans_clothes:{keywords:["fashion","shopping_bags","female"],char:'\u{1f45a}',fitzpatrick_scale:!1,category:"people"},tshirt:{keywords:["fashion","cloth","casual","shirt","tee"],char:'\u{1f455}',fitzpatrick_scale:!1,category:"people"},jeans:{keywords:["fashion","shopping"],char:'\u{1f456}',fitzpatrick_scale:!1,category:"people"},necktie:{keywords:["shirt","suitup","formal","fashion","cloth","business"],char:'\u{1f454}',fitzpatrick_scale:!1,category:"people"},dress:{keywords:["clothes","fashion","shopping"],char:'\u{1f457}',fitzpatrick_scale:!1,category:"people"},bikini:{keywords:["swimming","female","woman","girl","fashion","beach","summer"],char:'\u{1f459}',fitzpatrick_scale:!1,category:"people"},kimono:{keywords:["dress","fashion","women","female","japanese"],char:'\u{1f458}',fitzpatrick_scale:!1,category:"people"},lipstick:{keywords:["female","girl","fashion","woman"],char:'\u{1f484}',fitzpatrick_scale:!1,category:"people"},kiss:{keywords:["face","lips","love","like","affection","valentines"],char:'\u{1f48b}',fitzpatrick_scale:!1,category:"people"},footprints:{keywords:["feet","tracking","walking","beach"],char:'\u{1f463}',fitzpatrick_scale:!1,category:"people"},flat_shoe:{keywords:["ballet","slip-on","slipper"],char:'\u{1f97f}',fitzpatrick_scale:!1,category:"people"},high_heel:{keywords:["fashion","shoes","female","pumps","stiletto"],char:'\u{1f460}',fitzpatrick_scale:!1,category:"people"},sandal:{keywords:["shoes","fashion","flip flops"],char:'\u{1f461}',fitzpatrick_scale:!1,category:"people"},boot:{keywords:["shoes","fashion"],char:'\u{1f462}',fitzpatrick_scale:!1,category:"people"},mans_shoe:{keywords:["fashion","male"],char:'\u{1f45e}',fitzpatrick_scale:!1,category:"people"},athletic_shoe:{keywords:["shoes","sports","sneakers"],char:'\u{1f45f}',fitzpatrick_scale:!1,category:"people"},hiking_boot:{keywords:["backpacking","camping","hiking"],char:'\u{1f97e}',fitzpatrick_scale:!1,category:"people"},socks:{keywords:["stockings","clothes"],char:'\u{1f9e6}',fitzpatrick_scale:!1,category:"people"},gloves:{keywords:["hands","winter","clothes"],char:'\u{1f9e4}',fitzpatrick_scale:!1,category:"people"},scarf:{keywords:["neck","winter","clothes"],char:'\u{1f9e3}',fitzpatrick_scale:!1,category:"people"},womans_hat:{keywords:["fashion","accessories","female","lady","spring"],char:'\u{1f452}',fitzpatrick_scale:!1,category:"people"},tophat:{keywords:["magic","gentleman","classy","circus"],char:'\u{1f3a9}',fitzpatrick_scale:!1,category:"people"},billed_hat:{keywords:["cap","baseball"],char:'\u{1f9e2}',fitzpatrick_scale:!1,category:"people"},rescue_worker_helmet:{keywords:["construction","build"],char:'\u26d1',fitzpatrick_scale:!1,category:"people"},mortar_board:{keywords:["school","college","degree","university","graduation","cap","hat","legal","learn","education"],char:'\u{1f393}',fitzpatrick_scale:!1,category:"people"},crown:{keywords:["king","kod","leader","royalty","lord"],char:'\u{1f451}',fitzpatrick_scale:!1,category:"people"},school_satchel:{keywords:["student","education","bag","backpack"],char:'\u{1f392}',fitzpatrick_scale:!1,category:"people"},luggage:{keywords:["packing","travel"],char:'\u{1f9f3}',fitzpatrick_scale:!1,category:"people"},pouch:{keywords:["bag","accessories","shopping"],char:'\u{1f45d}',fitzpatrick_scale:!1,category:"people"},purse:{keywords:["fashion","accessories","money","sales","shopping"],char:'\u{1f45b}',fitzpatrick_scale:!1,category:"people"},handbag:{keywords:["fashion","accessory","accessories","shopping"],char:'\u{1f45c}',fitzpatrick_scale:!1,category:"people"},briefcase:{keywords:["business","documents","work","law","legal","job","career"],char:'\u{1f4bc}',fitzpatrick_scale:!1,category:"people"},eyeglasses:{keywords:["fashion","accessories","eyesight","nerdy","dork","geek"],char:'\u{1f453}',fitzpatrick_scale:!1,category:"people"},dark_sunglasses:{keywords:["face","cool","accessories"],char:'\u{1f576}',fitzpatrick_scale:!1,category:"people"},goggles:{keywords:["eyes","protection","safety"],char:'\u{1f97d}',fitzpatrick_scale:!1,category:"people"},ring:{keywords:["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"],char:'\u{1f48d}',fitzpatrick_scale:!1,category:"people"},closed_umbrella:{keywords:["weather","rain","drizzle"],char:'\u{1f302}',fitzpatrick_scale:!1,category:"people"},dog:{keywords:["animal","friend","nature","woof","puppy","pet","faithful"],char:'\u{1f436}',fitzpatrick_scale:!1,category:"animals_and_nature"},cat:{keywords:["animal","meow","nature","pet","kitten"],char:'\u{1f431}',fitzpatrick_scale:!1,category:"animals_and_nature"},mouse:{keywords:["animal","nature","cheese_wedge","rodent"],char:'\u{1f42d}',fitzpatrick_scale:!1,category:"animals_and_nature"},hamster:{keywords:["animal","nature"],char:'\u{1f439}',fitzpatrick_scale:!1,category:"animals_and_nature"},rabbit:{keywords:["animal","nature","pet","spring","magic","bunny"],char:'\u{1f430}',fitzpatrick_scale:!1,category:"animals_and_nature"},fox_face:{keywords:["animal","nature","face"],char:'\u{1f98a}',fitzpatrick_scale:!1,category:"animals_and_nature"},bear:{keywords:["animal","nature","wild"],char:'\u{1f43b}',fitzpatrick_scale:!1,category:"animals_and_nature"},panda_face:{keywords:["animal","nature","panda"],char:'\u{1f43c}',fitzpatrick_scale:!1,category:"animals_and_nature"},koala:{keywords:["animal","nature"],char:'\u{1f428}',fitzpatrick_scale:!1,category:"animals_and_nature"},tiger:{keywords:["animal","cat","danger","wild","nature","roar"],char:'\u{1f42f}',fitzpatrick_scale:!1,category:"animals_and_nature"},lion:{keywords:["animal","nature"],char:'\u{1f981}',fitzpatrick_scale:!1,category:"animals_and_nature"},cow:{keywords:["beef","ox","animal","nature","moo","milk"],char:'\u{1f42e}',fitzpatrick_scale:!1,category:"animals_and_nature"},pig:{keywords:["animal","oink","nature"],char:'\u{1f437}',fitzpatrick_scale:!1,category:"animals_and_nature"},pig_nose:{keywords:["animal","oink"],char:'\u{1f43d}',fitzpatrick_scale:!1,category:"animals_and_nature"},frog:{keywords:["animal","nature","croak","toad"],char:'\u{1f438}',fitzpatrick_scale:!1,category:"animals_and_nature"},squid:{keywords:["animal","nature","ocean","sea"],char:'\u{1f991}',fitzpatrick_scale:!1,category:"animals_and_nature"},octopus:{keywords:["animal","creature","ocean","sea","nature","beach"],char:'\u{1f419}',fitzpatrick_scale:!1,category:"animals_and_nature"},shrimp:{keywords:["animal","ocean","nature","seafood"],char:'\u{1f990}',fitzpatrick_scale:!1,category:"animals_and_nature"},monkey_face:{keywords:["animal","nature","circus"],char:'\u{1f435}',fitzpatrick_scale:!1,category:"animals_and_nature"},gorilla:{keywords:["animal","nature","circus"],char:'\u{1f98d}',fitzpatrick_scale:!1,category:"animals_and_nature"},see_no_evil:{keywords:["monkey","animal","nature","haha"],char:'\u{1f648}',fitzpatrick_scale:!1,category:"animals_and_nature"},hear_no_evil:{keywords:["animal","monkey","nature"],char:'\u{1f649}',fitzpatrick_scale:!1,category:"animals_and_nature"},speak_no_evil:{keywords:["monkey","animal","nature","omg"],char:'\u{1f64a}',fitzpatrick_scale:!1,category:"animals_and_nature"},monkey:{keywords:["animal","nature","banana","circus"],char:'\u{1f412}',fitzpatrick_scale:!1,category:"animals_and_nature"},chicken:{keywords:["animal","cluck","nature","bird"],char:'\u{1f414}',fitzpatrick_scale:!1,category:"animals_and_nature"},penguin:{keywords:["animal","nature"],char:'\u{1f427}',fitzpatrick_scale:!1,category:"animals_and_nature"},bird:{keywords:["animal","nature","fly","tweet","spring"],char:'\u{1f426}',fitzpatrick_scale:!1,category:"animals_and_nature"},baby_chick:{keywords:["animal","chicken","bird"],char:'\u{1f424}',fitzpatrick_scale:!1,category:"animals_and_nature"},hatching_chick:{keywords:["animal","chicken","egg","born","baby","bird"],char:'\u{1f423}',fitzpatrick_scale:!1,category:"animals_and_nature"},hatched_chick:{keywords:["animal","chicken","baby","bird"],char:'\u{1f425}',fitzpatrick_scale:!1,category:"animals_and_nature"},duck:{keywords:["animal","nature","bird","mallard"],char:'\u{1f986}',fitzpatrick_scale:!1,category:"animals_and_nature"},eagle:{keywords:["animal","nature","bird"],char:'\u{1f985}',fitzpatrick_scale:!1,category:"animals_and_nature"},owl:{keywords:["animal","nature","bird","hoot"],char:'\u{1f989}',fitzpatrick_scale:!1,category:"animals_and_nature"},bat:{keywords:["animal","nature","blind","vampire"],char:'\u{1f987}',fitzpatrick_scale:!1,category:"animals_and_nature"},wolf:{keywords:["animal","nature","wild"],char:'\u{1f43a}',fitzpatrick_scale:!1,category:"animals_and_nature"},boar:{keywords:["animal","nature"],char:'\u{1f417}',fitzpatrick_scale:!1,category:"animals_and_nature"},horse:{keywords:["animal","brown","nature"],char:'\u{1f434}',fitzpatrick_scale:!1,category:"animals_and_nature"},unicorn:{keywords:["animal","nature","mystical"],char:'\u{1f984}',fitzpatrick_scale:!1,category:"animals_and_nature"},honeybee:{keywords:["animal","insect","nature","bug","spring","honey"],char:'\u{1f41d}',fitzpatrick_scale:!1,category:"animals_and_nature"},bug:{keywords:["animal","insect","nature","worm"],char:'\u{1f41b}',fitzpatrick_scale:!1,category:"animals_and_nature"},butterfly:{keywords:["animal","insect","nature","caterpillar"],char:'\u{1f98b}',fitzpatrick_scale:!1,category:"animals_and_nature"},snail:{keywords:["slow","animal","shell"],char:'\u{1f40c}',fitzpatrick_scale:!1,category:"animals_and_nature"},beetle:{keywords:["animal","insect","nature","ladybug"],char:'\u{1f41e}',fitzpatrick_scale:!1,category:"animals_and_nature"},ant:{keywords:["animal","insect","nature","bug"],char:'\u{1f41c}',fitzpatrick_scale:!1,category:"animals_and_nature"},grasshopper:{keywords:["animal","cricket","chirp"],char:'\u{1f997}',fitzpatrick_scale:!1,category:"animals_and_nature"},spider:{keywords:["animal","arachnid"],char:'\u{1f577}',fitzpatrick_scale:!1,category:"animals_and_nature"},scorpion:{keywords:["animal","arachnid"],char:'\u{1f982}',fitzpatrick_scale:!1,category:"animals_and_nature"},crab:{keywords:["animal","crustacean"],char:'\u{1f980}',fitzpatrick_scale:!1,category:"animals_and_nature"},snake:{keywords:["animal","evil","nature","hiss","python"],char:'\u{1f40d}',fitzpatrick_scale:!1,category:"animals_and_nature"},lizard:{keywords:["animal","nature","reptile"],char:'\u{1f98e}',fitzpatrick_scale:!1,category:"animals_and_nature"},"t-rex":{keywords:["animal","nature","dinosaur","tyrannosaurus","extinct"],char:'\u{1f996}',fitzpatrick_scale:!1,category:"animals_and_nature"},sauropod:{keywords:["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"],char:'\u{1f995}',fitzpatrick_scale:!1,category:"animals_and_nature"},turtle:{keywords:["animal","slow","nature","tortoise"],char:'\u{1f422}',fitzpatrick_scale:!1,category:"animals_and_nature"},tropical_fish:{keywords:["animal","swim","ocean","beach","nemo"],char:'\u{1f420}',fitzpatrick_scale:!1,category:"animals_and_nature"},fish:{keywords:["animal","food","nature"],char:'\u{1f41f}',fitzpatrick_scale:!1,category:"animals_and_nature"},blowfish:{keywords:["animal","nature","food","sea","ocean"],char:'\u{1f421}',fitzpatrick_scale:!1,category:"animals_and_nature"},dolphin:{keywords:["animal","nature","fish","sea","ocean","flipper","fins","beach"],char:'\u{1f42c}',fitzpatrick_scale:!1,category:"animals_and_nature"},shark:{keywords:["animal","nature","fish","sea","ocean","jaws","fins","beach"],char:'\u{1f988}',fitzpatrick_scale:!1,category:"animals_and_nature"},whale:{keywords:["animal","nature","sea","ocean"],char:'\u{1f433}',fitzpatrick_scale:!1,category:"animals_and_nature"},whale2:{keywords:["animal","nature","sea","ocean"],char:'\u{1f40b}',fitzpatrick_scale:!1,category:"animals_and_nature"},crocodile:{keywords:["animal","nature","reptile","lizard","alligator"],char:'\u{1f40a}',fitzpatrick_scale:!1,category:"animals_and_nature"},leopard:{keywords:["animal","nature"],char:'\u{1f406}',fitzpatrick_scale:!1,category:"animals_and_nature"},zebra:{keywords:["animal","nature","stripes","safari"],char:'\u{1f993}',fitzpatrick_scale:!1,category:"animals_and_nature"},tiger2:{keywords:["animal","nature","roar"],char:'\u{1f405}',fitzpatrick_scale:!1,category:"animals_and_nature"},water_buffalo:{keywords:["animal","nature","ox","cow"],char:'\u{1f403}',fitzpatrick_scale:!1,category:"animals_and_nature"},ox:{keywords:["animal","cow","beef"],char:'\u{1f402}',fitzpatrick_scale:!1,category:"animals_and_nature"},cow2:{keywords:["beef","ox","animal","nature","moo","milk"],char:'\u{1f404}',fitzpatrick_scale:!1,category:"animals_and_nature"},deer:{keywords:["animal","nature","horns","venison"],char:'\u{1f98c}',fitzpatrick_scale:!1,category:"animals_and_nature"},dromedary_camel:{keywords:["animal","hot","desert","hump"],char:'\u{1f42a}',fitzpatrick_scale:!1,category:"animals_and_nature"},camel:{keywords:["animal","nature","hot","desert","hump"],char:'\u{1f42b}',fitzpatrick_scale:!1,category:"animals_and_nature"},giraffe:{keywords:["animal","nature","spots","safari"],char:'\u{1f992}',fitzpatrick_scale:!1,category:"animals_and_nature"},elephant:{keywords:["animal","nature","nose","th","circus"],char:'\u{1f418}',fitzpatrick_scale:!1,category:"animals_and_nature"},rhinoceros:{keywords:["animal","nature","horn"],char:'\u{1f98f}',fitzpatrick_scale:!1,category:"animals_and_nature"},goat:{keywords:["animal","nature"],char:'\u{1f410}',fitzpatrick_scale:!1,category:"animals_and_nature"},ram:{keywords:["animal","sheep","nature"],char:'\u{1f40f}',fitzpatrick_scale:!1,category:"animals_and_nature"},sheep:{keywords:["animal","nature","wool","shipit"],char:'\u{1f411}',fitzpatrick_scale:!1,category:"animals_and_nature"},racehorse:{keywords:["animal","gamble","luck"],char:'\u{1f40e}',fitzpatrick_scale:!1,category:"animals_and_nature"},pig2:{keywords:["animal","nature"],char:'\u{1f416}',fitzpatrick_scale:!1,category:"animals_and_nature"},rat:{keywords:["animal","mouse","rodent"],char:'\u{1f400}',fitzpatrick_scale:!1,category:"animals_and_nature"},mouse2:{keywords:["animal","nature","rodent"],char:'\u{1f401}',fitzpatrick_scale:!1,category:"animals_and_nature"},rooster:{keywords:["animal","nature","chicken"],char:'\u{1f413}',fitzpatrick_scale:!1,category:"animals_and_nature"},turkey:{keywords:["animal","bird"],char:'\u{1f983}',fitzpatrick_scale:!1,category:"animals_and_nature"},dove:{keywords:["animal","bird"],char:'\u{1f54a}',fitzpatrick_scale:!1,category:"animals_and_nature"},dog2:{keywords:["animal","nature","friend","doge","pet","faithful"],char:'\u{1f415}',fitzpatrick_scale:!1,category:"animals_and_nature"},poodle:{keywords:["dog","animal","101","nature","pet"],char:'\u{1f429}',fitzpatrick_scale:!1,category:"animals_and_nature"},cat2:{keywords:["animal","meow","pet","cats"],char:'\u{1f408}',fitzpatrick_scale:!1,category:"animals_and_nature"},rabbit2:{keywords:["animal","nature","pet","magic","spring"],char:'\u{1f407}',fitzpatrick_scale:!1,category:"animals_and_nature"},chipmunk:{keywords:["animal","nature","rodent","squirrel"],char:'\u{1f43f}',fitzpatrick_scale:!1,category:"animals_and_nature"},hedgehog:{keywords:["animal","nature","spiny"],char:'\u{1f994}',fitzpatrick_scale:!1,category:"animals_and_nature"},raccoon:{keywords:["animal","nature"],char:'\u{1f99d}',fitzpatrick_scale:!1,category:"animals_and_nature"},llama:{keywords:["animal","nature","alpaca"],char:'\u{1f999}',fitzpatrick_scale:!1,category:"animals_and_nature"},hippopotamus:{keywords:["animal","nature"],char:'\u{1f99b}',fitzpatrick_scale:!1,category:"animals_and_nature"},kangaroo:{keywords:["animal","nature","australia","joey","hop","marsupial"],char:'\u{1f998}',fitzpatrick_scale:!1,category:"animals_and_nature"},badger:{keywords:["animal","nature","honey"],char:'\u{1f9a1}',fitzpatrick_scale:!1,category:"animals_and_nature"},swan:{keywords:["animal","nature","bird"],char:'\u{1f9a2}',fitzpatrick_scale:!1,category:"animals_and_nature"},peacock:{keywords:["animal","nature","peahen","bird"],char:'\u{1f99a}',fitzpatrick_scale:!1,category:"animals_and_nature"},parrot:{keywords:["animal","nature","bird","pirate","talk"],char:'\u{1f99c}',fitzpatrick_scale:!1,category:"animals_and_nature"},lobster:{keywords:["animal","nature","bisque","claws","seafood"],char:'\u{1f99e}',fitzpatrick_scale:!1,category:"animals_and_nature"},mosquito:{keywords:["animal","nature","insect","malaria"],char:'\u{1f99f}',fitzpatrick_scale:!1,category:"animals_and_nature"},paw_prints:{keywords:["animal","tracking","footprints","dog","cat","pet","feet"],char:'\u{1f43e}',fitzpatrick_scale:!1,category:"animals_and_nature"},dragon:{keywords:["animal","myth","nature","chinese","green"],char:'\u{1f409}',fitzpatrick_scale:!1,category:"animals_and_nature"},dragon_face:{keywords:["animal","myth","nature","chinese","green"],char:'\u{1f432}',fitzpatrick_scale:!1,category:"animals_and_nature"},cactus:{keywords:["vegetable","plant","nature"],char:'\u{1f335}',fitzpatrick_scale:!1,category:"animals_and_nature"},christmas_tree:{keywords:["festival","vacation","december","xmas","celebration"],char:'\u{1f384}',fitzpatrick_scale:!1,category:"animals_and_nature"},evergreen_tree:{keywords:["plant","nature"],char:'\u{1f332}',fitzpatrick_scale:!1,category:"animals_and_nature"},deciduous_tree:{keywords:["plant","nature"],char:'\u{1f333}',fitzpatrick_scale:!1,category:"animals_and_nature"},palm_tree:{keywords:["plant","vegetable","nature","summer","beach","mojito","tropical"],char:'\u{1f334}',fitzpatrick_scale:!1,category:"animals_and_nature"},seedling:{keywords:["plant","nature","grass","lawn","spring"],char:'\u{1f331}',fitzpatrick_scale:!1,category:"animals_and_nature"},herb:{keywords:["vegetable","plant","medicine","weed","grass","lawn"],char:'\u{1f33f}',fitzpatrick_scale:!1,category:"animals_and_nature"},shamrock:{keywords:["vegetable","plant","nature","irish","clover"],char:'\u2618',fitzpatrick_scale:!1,category:"animals_and_nature"},four_leaf_clover:{keywords:["vegetable","plant","nature","lucky","irish"],char:'\u{1f340}',fitzpatrick_scale:!1,category:"animals_and_nature"},bamboo:{keywords:["plant","nature","vegetable","panda","pine_decoration"],char:'\u{1f38d}',fitzpatrick_scale:!1,category:"animals_and_nature"},tanabata_tree:{keywords:["plant","nature","branch","summer"],char:'\u{1f38b}',fitzpatrick_scale:!1,category:"animals_and_nature"},leaves:{keywords:["nature","plant","tree","vegetable","grass","lawn","spring"],char:'\u{1f343}',fitzpatrick_scale:!1,category:"animals_and_nature"},fallen_leaf:{keywords:["nature","plant","vegetable","leaves"],char:'\u{1f342}',fitzpatrick_scale:!1,category:"animals_and_nature"},maple_leaf:{keywords:["nature","plant","vegetable","ca","fall"],char:'\u{1f341}',fitzpatrick_scale:!1,category:"animals_and_nature"},ear_of_rice:{keywords:["nature","plant"],char:'\u{1f33e}',fitzpatrick_scale:!1,category:"animals_and_nature"},hibiscus:{keywords:["plant","vegetable","flowers","beach"],char:'\u{1f33a}',fitzpatrick_scale:!1,category:"animals_and_nature"},sunflower:{keywords:["nature","plant","fall"],char:'\u{1f33b}',fitzpatrick_scale:!1,category:"animals_and_nature"},rose:{keywords:["flowers","valentines","love","spring"],char:'\u{1f339}',fitzpatrick_scale:!1,category:"animals_and_nature"},wilted_flower:{keywords:["plant","nature","flower"],char:'\u{1f940}',fitzpatrick_scale:!1,category:"animals_and_nature"},tulip:{keywords:["flowers","plant","nature","summer","spring"],char:'\u{1f337}',fitzpatrick_scale:!1,category:"animals_and_nature"},blossom:{keywords:["nature","flowers","yellow"],char:'\u{1f33c}',fitzpatrick_scale:!1,category:"animals_and_nature"},cherry_blossom:{keywords:["nature","plant","spring","flower"],char:'\u{1f338}',fitzpatrick_scale:!1,category:"animals_and_nature"},bouquet:{keywords:["flowers","nature","spring"],char:'\u{1f490}',fitzpatrick_scale:!1,category:"animals_and_nature"},mushroom:{keywords:["plant","vegetable"],char:'\u{1f344}',fitzpatrick_scale:!1,category:"animals_and_nature"},chestnut:{keywords:["food","squirrel"],char:'\u{1f330}',fitzpatrick_scale:!1,category:"animals_and_nature"},jack_o_lantern:{keywords:["halloween","light","pumpkin","creepy","fall"],char:'\u{1f383}',fitzpatrick_scale:!1,category:"animals_and_nature"},shell:{keywords:["nature","sea","beach"],char:'\u{1f41a}',fitzpatrick_scale:!1,category:"animals_and_nature"},spider_web:{keywords:["animal","insect","arachnid","silk"],char:'\u{1f578}',fitzpatrick_scale:!1,category:"animals_and_nature"},earth_americas:{keywords:["globe","world","USA","international"],char:'\u{1f30e}',fitzpatrick_scale:!1,category:"animals_and_nature"},earth_africa:{keywords:["globe","world","international"],char:'\u{1f30d}',fitzpatrick_scale:!1,category:"animals_and_nature"},earth_asia:{keywords:["globe","world","east","international"],char:'\u{1f30f}',fitzpatrick_scale:!1,category:"animals_and_nature"},full_moon:{keywords:["nature","yellow","twilight","planet","space","night","evening","sleep"],char:'\u{1f315}',fitzpatrick_scale:!1,category:"animals_and_nature"},waning_gibbous_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"],char:'\u{1f316}',fitzpatrick_scale:!1,category:"animals_and_nature"},last_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f317}',fitzpatrick_scale:!1,category:"animals_and_nature"},waning_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f318}',fitzpatrick_scale:!1,category:"animals_and_nature"},new_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f311}',fitzpatrick_scale:!1,category:"animals_and_nature"},waxing_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f312}',fitzpatrick_scale:!1,category:"animals_and_nature"},first_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f313}',fitzpatrick_scale:!1,category:"animals_and_nature"},waxing_gibbous_moon:{keywords:["nature","night","sky","gray","twilight","planet","space","evening","sleep"],char:'\u{1f314}',fitzpatrick_scale:!1,category:"animals_and_nature"},new_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f31a}',fitzpatrick_scale:!1,category:"animals_and_nature"},full_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f31d}',fitzpatrick_scale:!1,category:"animals_and_nature"},first_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f31b}',fitzpatrick_scale:!1,category:"animals_and_nature"},last_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:'\u{1f31c}',fitzpatrick_scale:!1,category:"animals_and_nature"},sun_with_face:{keywords:["nature","morning","sky"],char:'\u{1f31e}',fitzpatrick_scale:!1,category:"animals_and_nature"},crescent_moon:{keywords:["night","sleep","sky","evening","magic"],char:'\u{1f319}',fitzpatrick_scale:!1,category:"animals_and_nature"},star:{keywords:["night","yellow"],char:'\u2b50',fitzpatrick_scale:!1,category:"animals_and_nature"},star2:{keywords:["night","sparkle","awesome","good","magic"],char:'\u{1f31f}',fitzpatrick_scale:!1,category:"animals_and_nature"},dizzy:{keywords:["star","sparkle","shoot","magic"],char:'\u{1f4ab}',fitzpatrick_scale:!1,category:"animals_and_nature"},sparkles:{keywords:["stars","shine","shiny","cool","awesome","good","magic"],char:'\u2728',fitzpatrick_scale:!1,category:"animals_and_nature"},comet:{keywords:["space"],char:'\u2604',fitzpatrick_scale:!1,category:"animals_and_nature"},sunny:{keywords:["weather","nature","brightness","summer","beach","spring"],char:'\u2600\ufe0f',fitzpatrick_scale:!1,category:"animals_and_nature"},sun_behind_small_cloud:{keywords:["weather"],char:'\u{1f324}',fitzpatrick_scale:!1,category:"animals_and_nature"},partly_sunny:{keywords:["weather","nature","cloudy","morning","fall","spring"],char:'\u26c5',fitzpatrick_scale:!1,category:"animals_and_nature"},sun_behind_large_cloud:{keywords:["weather"],char:'\u{1f325}',fitzpatrick_scale:!1,category:"animals_and_nature"},sun_behind_rain_cloud:{keywords:["weather"],char:'\u{1f326}',fitzpatrick_scale:!1,category:"animals_and_nature"},cloud:{keywords:["weather","sky"],char:'\u2601\ufe0f',fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_rain:{keywords:["weather"],char:'\u{1f327}',fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_lightning_and_rain:{keywords:["weather","lightning"],char:'\u26c8',fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_lightning:{keywords:["weather","thunder"],char:'\u{1f329}',fitzpatrick_scale:!1,category:"animals_and_nature"},zap:{keywords:["thunder","weather","lightning bolt","fast"],char:'\u26a1',fitzpatrick_scale:!1,category:"animals_and_nature"},fire:{keywords:["hot","cook","flame"],char:'\u{1f525}',fitzpatrick_scale:!1,category:"animals_and_nature"},boom:{keywords:["bomb","explode","explosion","collision","blown"],char:'\u{1f4a5}',fitzpatrick_scale:!1,category:"animals_and_nature"},snowflake:{keywords:["winter","season","cold","weather","christmas","xmas"],char:'\u2744\ufe0f',fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_snow:{keywords:["weather"],char:'\u{1f328}',fitzpatrick_scale:!1,category:"animals_and_nature"},snowman:{keywords:["winter","season","cold","weather","christmas","xmas","frozen","without_snow"],char:'\u26c4',fitzpatrick_scale:!1,category:"animals_and_nature"},snowman_with_snow:{keywords:["winter","season","cold","weather","christmas","xmas","frozen"],char:'\u2603',fitzpatrick_scale:!1,category:"animals_and_nature"},wind_face:{keywords:["gust","air"],char:'\u{1f32c}',fitzpatrick_scale:!1,category:"animals_and_nature"},dash:{keywords:["wind","air","fast","shoo","fart","smoke","puff"],char:'\u{1f4a8}',fitzpatrick_scale:!1,category:"animals_and_nature"},tornado:{keywords:["weather","cyclone","twister"],char:'\u{1f32a}',fitzpatrick_scale:!1,category:"animals_and_nature"},fog:{keywords:["weather"],char:'\u{1f32b}',fitzpatrick_scale:!1,category:"animals_and_nature"},open_umbrella:{keywords:["weather","spring"],char:'\u2602',fitzpatrick_scale:!1,category:"animals_and_nature"},umbrella:{keywords:["rainy","weather","spring"],char:'\u2614',fitzpatrick_scale:!1,category:"animals_and_nature"},droplet:{keywords:["water","drip","faucet","spring"],char:'\u{1f4a7}',fitzpatrick_scale:!1,category:"animals_and_nature"},sweat_drops:{keywords:["water","drip","oops"],char:'\u{1f4a6}',fitzpatrick_scale:!1,category:"animals_and_nature"},ocean:{keywords:["sea","water","wave","nature","tsunami","disaster"],char:'\u{1f30a}',fitzpatrick_scale:!1,category:"animals_and_nature"},green_apple:{keywords:["fruit","nature"],char:'\u{1f34f}',fitzpatrick_scale:!1,category:"food_and_drink"},apple:{keywords:["fruit","mac","school"],char:'\u{1f34e}',fitzpatrick_scale:!1,category:"food_and_drink"},pear:{keywords:["fruit","nature","food"],char:'\u{1f350}',fitzpatrick_scale:!1,category:"food_and_drink"},tangerine:{keywords:["food","fruit","nature","orange"],char:'\u{1f34a}',fitzpatrick_scale:!1,category:"food_and_drink"},lemon:{keywords:["fruit","nature"],char:'\u{1f34b}',fitzpatrick_scale:!1,category:"food_and_drink"},banana:{keywords:["fruit","food","monkey"],char:'\u{1f34c}',fitzpatrick_scale:!1,category:"food_and_drink"},watermelon:{keywords:["fruit","food","picnic","summer"],char:'\u{1f349}',fitzpatrick_scale:!1,category:"food_and_drink"},grapes:{keywords:["fruit","food","wine"],char:'\u{1f347}',fitzpatrick_scale:!1,category:"food_and_drink"},strawberry:{keywords:["fruit","food","nature"],char:'\u{1f353}',fitzpatrick_scale:!1,category:"food_and_drink"},melon:{keywords:["fruit","nature","food"],char:'\u{1f348}',fitzpatrick_scale:!1,category:"food_and_drink"},cherries:{keywords:["food","fruit"],char:'\u{1f352}',fitzpatrick_scale:!1,category:"food_and_drink"},peach:{keywords:["fruit","nature","food"],char:'\u{1f351}',fitzpatrick_scale:!1,category:"food_and_drink"},pineapple:{keywords:["fruit","nature","food"],char:'\u{1f34d}',fitzpatrick_scale:!1,category:"food_and_drink"},coconut:{keywords:["fruit","nature","food","palm"],char:'\u{1f965}',fitzpatrick_scale:!1,category:"food_and_drink"},kiwi_fruit:{keywords:["fruit","food"],char:'\u{1f95d}',fitzpatrick_scale:!1,category:"food_and_drink"},mango:{keywords:["fruit","food","tropical"],char:'\u{1f96d}',fitzpatrick_scale:!1,category:"food_and_drink"},avocado:{keywords:["fruit","food"],char:'\u{1f951}',fitzpatrick_scale:!1,category:"food_and_drink"},broccoli:{keywords:["fruit","food","vegetable"],char:'\u{1f966}',fitzpatrick_scale:!1,category:"food_and_drink"},tomato:{keywords:["fruit","vegetable","nature","food"],char:'\u{1f345}',fitzpatrick_scale:!1,category:"food_and_drink"},eggplant:{keywords:["vegetable","nature","food","aubergine"],char:'\u{1f346}',fitzpatrick_scale:!1,category:"food_and_drink"},cucumber:{keywords:["fruit","food","pickle"],char:'\u{1f952}',fitzpatrick_scale:!1,category:"food_and_drink"},carrot:{keywords:["vegetable","food","orange"],char:'\u{1f955}',fitzpatrick_scale:!1,category:"food_and_drink"},hot_pepper:{keywords:["food","spicy","chilli","chili"],char:'\u{1f336}',fitzpatrick_scale:!1,category:"food_and_drink"},potato:{keywords:["food","tuber","vegatable","starch"],char:'\u{1f954}',fitzpatrick_scale:!1,category:"food_and_drink"},corn:{keywords:["food","vegetable","plant"],char:'\u{1f33d}',fitzpatrick_scale:!1,category:"food_and_drink"},leafy_greens:{keywords:["food","vegetable","plant","bok choy","cabbage","kale","lettuce"],char:'\u{1f96c}',fitzpatrick_scale:!1,category:"food_and_drink"},sweet_potato:{keywords:["food","nature"],char:'\u{1f360}',fitzpatrick_scale:!1,category:"food_and_drink"},peanuts:{keywords:["food","nut"],char:'\u{1f95c}',fitzpatrick_scale:!1,category:"food_and_drink"},honey_pot:{keywords:["bees","sweet","kitchen"],char:'\u{1f36f}',fitzpatrick_scale:!1,category:"food_and_drink"},croissant:{keywords:["food","bread","french"],char:'\u{1f950}',fitzpatrick_scale:!1,category:"food_and_drink"},bread:{keywords:["food","wheat","breakfast","toast"],char:'\u{1f35e}',fitzpatrick_scale:!1,category:"food_and_drink"},baguette_bread:{keywords:["food","bread","french"],char:'\u{1f956}',fitzpatrick_scale:!1,category:"food_and_drink"},bagel:{keywords:["food","bread","bakery","schmear"],char:'\u{1f96f}',fitzpatrick_scale:!1,category:"food_and_drink"},pretzel:{keywords:["food","bread","twisted"],char:'\u{1f968}',fitzpatrick_scale:!1,category:"food_and_drink"},cheese:{keywords:["food","chadder"],char:'\u{1f9c0}',fitzpatrick_scale:!1,category:"food_and_drink"},egg:{keywords:["food","chicken","breakfast"],char:'\u{1f95a}',fitzpatrick_scale:!1,category:"food_and_drink"},bacon:{keywords:["food","breakfast","pork","pig","meat"],char:'\u{1f953}',fitzpatrick_scale:!1,category:"food_and_drink"},steak:{keywords:["food","cow","meat","cut","chop","lambchop","porkchop"],char:'\u{1f969}',fitzpatrick_scale:!1,category:"food_and_drink"},pancakes:{keywords:["food","breakfast","flapjacks","hotcakes"],char:'\u{1f95e}',fitzpatrick_scale:!1,category:"food_and_drink"},poultry_leg:{keywords:["food","meat","drumstick","bird","chicken","turkey"],char:'\u{1f357}',fitzpatrick_scale:!1,category:"food_and_drink"},meat_on_bone:{keywords:["good","food","drumstick"],char:'\u{1f356}',fitzpatrick_scale:!1,category:"food_and_drink"},bone:{keywords:["skeleton"],char:'\u{1f9b4}',fitzpatrick_scale:!1,category:"food_and_drink"},fried_shrimp:{keywords:["food","animal","appetizer","summer"],char:'\u{1f364}',fitzpatrick_scale:!1,category:"food_and_drink"},fried_egg:{keywords:["food","breakfast","kitchen","egg"],char:'\u{1f373}',fitzpatrick_scale:!1,category:"food_and_drink"},hamburger:{keywords:["meat","fast food","beef","cheeseburger","mcdonalds","burger king"],char:'\u{1f354}',fitzpatrick_scale:!1,category:"food_and_drink"},fries:{keywords:["chips","snack","fast food"],char:'\u{1f35f}',fitzpatrick_scale:!1,category:"food_and_drink"},stuffed_flatbread:{keywords:["food","flatbread","stuffed","gyro"],char:'\u{1f959}',fitzpatrick_scale:!1,category:"food_and_drink"},hotdog:{keywords:["food","frankfurter"],char:'\u{1f32d}',fitzpatrick_scale:!1,category:"food_and_drink"},pizza:{keywords:["food","party"],char:'\u{1f355}',fitzpatrick_scale:!1,category:"food_and_drink"},sandwich:{keywords:["food","lunch","bread"],char:'\u{1f96a}',fitzpatrick_scale:!1,category:"food_and_drink"},canned_food:{keywords:["food","soup"],char:'\u{1f96b}',fitzpatrick_scale:!1,category:"food_and_drink"},spaghetti:{keywords:["food","italian","noodle"],char:'\u{1f35d}',fitzpatrick_scale:!1,category:"food_and_drink"},taco:{keywords:["food","mexican"],char:'\u{1f32e}',fitzpatrick_scale:!1,category:"food_and_drink"},burrito:{keywords:["food","mexican"],char:'\u{1f32f}',fitzpatrick_scale:!1,category:"food_and_drink"},green_salad:{keywords:["food","healthy","lettuce"],char:'\u{1f957}',fitzpatrick_scale:!1,category:"food_and_drink"},shallow_pan_of_food:{keywords:["food","cooking","casserole","paella"],char:'\u{1f958}',fitzpatrick_scale:!1,category:"food_and_drink"},ramen:{keywords:["food","japanese","noodle","chopsticks"],char:'\u{1f35c}',fitzpatrick_scale:!1,category:"food_and_drink"},stew:{keywords:["food","meat","soup"],char:'\u{1f372}',fitzpatrick_scale:!1,category:"food_and_drink"},fish_cake:{keywords:["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"],char:'\u{1f365}',fitzpatrick_scale:!1,category:"food_and_drink"},fortune_cookie:{keywords:["food","prophecy"],char:'\u{1f960}',fitzpatrick_scale:!1,category:"food_and_drink"},sushi:{keywords:["food","fish","japanese","rice"],char:'\u{1f363}',fitzpatrick_scale:!1,category:"food_and_drink"},bento:{keywords:["food","japanese","box"],char:'\u{1f371}',fitzpatrick_scale:!1,category:"food_and_drink"},curry:{keywords:["food","spicy","hot","indian"],char:'\u{1f35b}',fitzpatrick_scale:!1,category:"food_and_drink"},rice_ball:{keywords:["food","japanese"],char:'\u{1f359}',fitzpatrick_scale:!1,category:"food_and_drink"},rice:{keywords:["food","china","asian"],char:'\u{1f35a}',fitzpatrick_scale:!1,category:"food_and_drink"},rice_cracker:{keywords:["food","japanese"],char:'\u{1f358}',fitzpatrick_scale:!1,category:"food_and_drink"},oden:{keywords:["food","japanese"],char:'\u{1f362}',fitzpatrick_scale:!1,category:"food_and_drink"},dango:{keywords:["food","dessert","sweet","japanese","barbecue","meat"],char:'\u{1f361}',fitzpatrick_scale:!1,category:"food_and_drink"},shaved_ice:{keywords:["hot","dessert","summer"],char:'\u{1f367}',fitzpatrick_scale:!1,category:"food_and_drink"},ice_cream:{keywords:["food","hot","dessert"],char:'\u{1f368}',fitzpatrick_scale:!1,category:"food_and_drink"},icecream:{keywords:["food","hot","dessert","summer"],char:'\u{1f366}',fitzpatrick_scale:!1,category:"food_and_drink"},pie:{keywords:["food","dessert","pastry"],char:'\u{1f967}',fitzpatrick_scale:!1,category:"food_and_drink"},cake:{keywords:["food","dessert"],char:'\u{1f370}',fitzpatrick_scale:!1,category:"food_and_drink"},cupcake:{keywords:["food","dessert","bakery","sweet"],char:'\u{1f9c1}',fitzpatrick_scale:!1,category:"food_and_drink"},moon_cake:{keywords:["food","autumn"],char:'\u{1f96e}',fitzpatrick_scale:!1,category:"food_and_drink"},birthday:{keywords:["food","dessert","cake"],char:'\u{1f382}',fitzpatrick_scale:!1,category:"food_and_drink"},custard:{keywords:["dessert","food"],char:'\u{1f36e}',fitzpatrick_scale:!1,category:"food_and_drink"},candy:{keywords:["snack","dessert","sweet","lolly"],char:'\u{1f36c}',fitzpatrick_scale:!1,category:"food_and_drink"},lollipop:{keywords:["food","snack","candy","sweet"],char:'\u{1f36d}',fitzpatrick_scale:!1,category:"food_and_drink"},chocolate_bar:{keywords:["food","snack","dessert","sweet"],char:'\u{1f36b}',fitzpatrick_scale:!1,category:"food_and_drink"},popcorn:{keywords:["food","movie theater","films","snack"],char:'\u{1f37f}',fitzpatrick_scale:!1,category:"food_and_drink"},dumpling:{keywords:["food","empanada","pierogi","potsticker"],char:'\u{1f95f}',fitzpatrick_scale:!1,category:"food_and_drink"},doughnut:{keywords:["food","dessert","snack","sweet","donut"],char:'\u{1f369}',fitzpatrick_scale:!1,category:"food_and_drink"},cookie:{keywords:["food","snack","oreo","chocolate","sweet","dessert"],char:'\u{1f36a}',fitzpatrick_scale:!1,category:"food_and_drink"},milk_glass:{keywords:["beverage","drink","cow"],char:'\u{1f95b}',fitzpatrick_scale:!1,category:"food_and_drink"},beer:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:'\u{1f37a}',fitzpatrick_scale:!1,category:"food_and_drink"},beers:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:'\u{1f37b}',fitzpatrick_scale:!1,category:"food_and_drink"},clinking_glasses:{keywords:["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"],char:'\u{1f942}',fitzpatrick_scale:!1,category:"food_and_drink"},wine_glass:{keywords:["drink","beverage","drunk","alcohol","booze"],char:'\u{1f377}',fitzpatrick_scale:!1,category:"food_and_drink"},tumbler_glass:{keywords:["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"],char:'\u{1f943}',fitzpatrick_scale:!1,category:"food_and_drink"},cocktail:{keywords:["drink","drunk","alcohol","beverage","booze","mojito"],char:'\u{1f378}',fitzpatrick_scale:!1,category:"food_and_drink"},tropical_drink:{keywords:["beverage","cocktail","summer","beach","alcohol","booze","mojito"],char:'\u{1f379}',fitzpatrick_scale:!1,category:"food_and_drink"},champagne:{keywords:["drink","wine","bottle","celebration"],char:'\u{1f37e}',fitzpatrick_scale:!1,category:"food_and_drink"},sake:{keywords:["wine","drink","drunk","beverage","japanese","alcohol","booze"],char:'\u{1f376}',fitzpatrick_scale:!1,category:"food_and_drink"},tea:{keywords:["drink","bowl","breakfast","green","british"],char:'\u{1f375}',fitzpatrick_scale:!1,category:"food_and_drink"},cup_with_straw:{keywords:["drink","soda"],char:'\u{1f964}',fitzpatrick_scale:!1,category:"food_and_drink"},coffee:{keywords:["beverage","caffeine","latte","espresso"],char:'\u2615',fitzpatrick_scale:!1,category:"food_and_drink"},baby_bottle:{keywords:["food","container","milk"],char:'\u{1f37c}',fitzpatrick_scale:!1,category:"food_and_drink"},salt:{keywords:["condiment","shaker"],char:'\u{1f9c2}',fitzpatrick_scale:!1,category:"food_and_drink"},spoon:{keywords:["cutlery","kitchen","tableware"],char:'\u{1f944}',fitzpatrick_scale:!1,category:"food_and_drink"},fork_and_knife:{keywords:["cutlery","kitchen"],char:'\u{1f374}',fitzpatrick_scale:!1,category:"food_and_drink"},plate_with_cutlery:{keywords:["food","eat","meal","lunch","dinner","restaurant"],char:'\u{1f37d}',fitzpatrick_scale:!1,category:"food_and_drink"},bowl_with_spoon:{keywords:["food","breakfast","cereal","oatmeal","porridge"],char:'\u{1f963}',fitzpatrick_scale:!1,category:"food_and_drink"},takeout_box:{keywords:["food","leftovers"],char:'\u{1f961}',fitzpatrick_scale:!1,category:"food_and_drink"},chopsticks:{keywords:["food"],char:'\u{1f962}',fitzpatrick_scale:!1,category:"food_and_drink"},soccer:{keywords:["sports","football"],char:'\u26bd',fitzpatrick_scale:!1,category:"activity"},basketball:{keywords:["sports","balls","NBA"],char:'\u{1f3c0}',fitzpatrick_scale:!1,category:"activity"},football:{keywords:["sports","balls","NFL"],char:'\u{1f3c8}',fitzpatrick_scale:!1,category:"activity"},baseball:{keywords:["sports","balls"],char:'\u26be',fitzpatrick_scale:!1,category:"activity"},softball:{keywords:["sports","balls"],char:'\u{1f94e}',fitzpatrick_scale:!1,category:"activity"},tennis:{keywords:["sports","balls","green"],char:'\u{1f3be}',fitzpatrick_scale:!1,category:"activity"},volleyball:{keywords:["sports","balls"],char:'\u{1f3d0}',fitzpatrick_scale:!1,category:"activity"},rugby_football:{keywords:["sports","team"],char:'\u{1f3c9}',fitzpatrick_scale:!1,category:"activity"},flying_disc:{keywords:["sports","frisbee","ultimate"],char:'\u{1f94f}',fitzpatrick_scale:!1,category:"activity"},"8ball":{keywords:["pool","hobby","game","luck","magic"],char:'\u{1f3b1}',fitzpatrick_scale:!1,category:"activity"},golf:{keywords:["sports","business","flag","hole","summer"],char:'\u26f3',fitzpatrick_scale:!1,category:"activity"},golfing_woman:{keywords:["sports","business","woman","female"],char:'\u{1f3cc}\ufe0f\u200d\u2640\ufe0f',fitzpatrick_scale:!1,category:"activity"},golfing_man:{keywords:["sports","business"],char:'\u{1f3cc}',fitzpatrick_scale:!0,category:"activity"},ping_pong:{keywords:["sports","pingpong"],char:'\u{1f3d3}',fitzpatrick_scale:!1,category:"activity"},badminton:{keywords:["sports"],char:'\u{1f3f8}',fitzpatrick_scale:!1,category:"activity"},goal_net:{keywords:["sports"],char:'\u{1f945}',fitzpatrick_scale:!1,category:"activity"},ice_hockey:{keywords:["sports"],char:'\u{1f3d2}',fitzpatrick_scale:!1,category:"activity"},field_hockey:{keywords:["sports"],char:'\u{1f3d1}',fitzpatrick_scale:!1,category:"activity"},lacrosse:{keywords:["sports","ball","stick"],char:'\u{1f94d}',fitzpatrick_scale:!1,category:"activity"},cricket:{keywords:["sports"],char:'\u{1f3cf}',fitzpatrick_scale:!1,category:"activity"},ski:{keywords:["sports","winter","cold","snow"],char:'\u{1f3bf}',fitzpatrick_scale:!1,category:"activity"},skier:{keywords:["sports","winter","snow"],char:'\u26f7',fitzpatrick_scale:!1,category:"activity"},snowboarder:{keywords:["sports","winter"],char:'\u{1f3c2}',fitzpatrick_scale:!0,category:"activity"},person_fencing:{keywords:["sports","fencing","sword"],char:'\u{1f93a}',fitzpatrick_scale:!1,category:"activity"},women_wrestling:{keywords:["sports","wrestlers"],char:'\u{1f93c}\u200d\u2640\ufe0f',fitzpatrick_scale:!1,category:"activity"},men_wrestling:{keywords:["sports","wrestlers"],char:'\u{1f93c}\u200d\u2642\ufe0f',fitzpatrick_scale:!1,category:"activity"},woman_cartwheeling:{keywords:["gymnastics"],char:'\u{1f938}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},man_cartwheeling:{keywords:["gymnastics"],char:'\u{1f938}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"activity"},woman_playing_handball:{keywords:["sports"],char:'\u{1f93e}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},man_playing_handball:{keywords:["sports"],char:'\u{1f93e}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"activity"},ice_skate:{keywords:["sports"],char:'\u26f8',fitzpatrick_scale:!1,category:"activity"},curling_stone:{keywords:["sports"],char:'\u{1f94c}',fitzpatrick_scale:!1,category:"activity"},skateboard:{keywords:["board"],char:'\u{1f6f9}',fitzpatrick_scale:!1,category:"activity"},sled:{keywords:["sleigh","luge","toboggan"],char:'\u{1f6f7}',fitzpatrick_scale:!1,category:"activity"},bow_and_arrow:{keywords:["sports"],char:'\u{1f3f9}',fitzpatrick_scale:!1,category:"activity"},fishing_pole_and_fish:{keywords:["food","hobby","summer"],char:'\u{1f3a3}',fitzpatrick_scale:!1,category:"activity"},boxing_glove:{keywords:["sports","fighting"],char:'\u{1f94a}',fitzpatrick_scale:!1,category:"activity"},martial_arts_uniform:{keywords:["judo","karate","taekwondo"],char:'\u{1f94b}',fitzpatrick_scale:!1,category:"activity"},rowing_woman:{keywords:["sports","hobby","water","ship","woman","female"],char:'\u{1f6a3}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},rowing_man:{keywords:["sports","hobby","water","ship"],char:'\u{1f6a3}',fitzpatrick_scale:!0,category:"activity"},climbing_woman:{keywords:["sports","hobby","woman","female","rock"],char:'\u{1f9d7}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},climbing_man:{keywords:["sports","hobby","man","male","rock"],char:'\u{1f9d7}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"activity"},swimming_woman:{keywords:["sports","exercise","human","athlete","water","summer","woman","female"],char:'\u{1f3ca}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},swimming_man:{keywords:["sports","exercise","human","athlete","water","summer"],char:'\u{1f3ca}',fitzpatrick_scale:!0,category:"activity"},woman_playing_water_polo:{keywords:["sports","pool"],char:'\u{1f93d}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},man_playing_water_polo:{keywords:["sports","pool"],char:'\u{1f93d}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"activity"},woman_in_lotus_position:{keywords:["woman","female","meditation","yoga","serenity","zen","mindfulness"],char:'\u{1f9d8}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},man_in_lotus_position:{keywords:["man","male","meditation","yoga","serenity","zen","mindfulness"],char:'\u{1f9d8}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"activity"},surfing_woman:{keywords:["sports","ocean","sea","summer","beach","woman","female"],char:'\u{1f3c4}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},surfing_man:{keywords:["sports","ocean","sea","summer","beach"],char:'\u{1f3c4}',fitzpatrick_scale:!0,category:"activity"},bath:{keywords:["clean","shower","bathroom"],char:'\u{1f6c0}',fitzpatrick_scale:!0,category:"activity"},basketball_woman:{keywords:["sports","human","woman","female"],char:'\u26f9\ufe0f\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},basketball_man:{keywords:["sports","human"],char:'\u26f9',fitzpatrick_scale:!0,category:"activity"},weight_lifting_woman:{keywords:["sports","training","exercise","woman","female"],char:'\u{1f3cb}\ufe0f\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},weight_lifting_man:{keywords:["sports","training","exercise"],char:'\u{1f3cb}',fitzpatrick_scale:!0,category:"activity"},biking_woman:{keywords:["sports","bike","exercise","hipster","woman","female"],char:'\u{1f6b4}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},biking_man:{keywords:["sports","bike","exercise","hipster"],char:'\u{1f6b4}',fitzpatrick_scale:!0,category:"activity"},mountain_biking_woman:{keywords:["transportation","sports","human","race","bike","woman","female"],char:'\u{1f6b5}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},mountain_biking_man:{keywords:["transportation","sports","human","race","bike"],char:'\u{1f6b5}',fitzpatrick_scale:!0,category:"activity"},horse_racing:{keywords:["animal","betting","competition","gambling","luck"],char:'\u{1f3c7}',fitzpatrick_scale:!0,category:"activity"},business_suit_levitating:{keywords:["suit","business","levitate","hover","jump"],char:'\u{1f574}',fitzpatrick_scale:!0,category:"activity"},trophy:{keywords:["win","award","contest","place","ftw","ceremony"],char:'\u{1f3c6}',fitzpatrick_scale:!1,category:"activity"},running_shirt_with_sash:{keywords:["play","pageant"],char:'\u{1f3bd}',fitzpatrick_scale:!1,category:"activity"},medal_sports:{keywords:["award","winning"],char:'\u{1f3c5}',fitzpatrick_scale:!1,category:"activity"},medal_military:{keywords:["award","winning","army"],char:'\u{1f396}',fitzpatrick_scale:!1,category:"activity"},"1st_place_medal":{keywords:["award","winning","first"],char:'\u{1f947}',fitzpatrick_scale:!1,category:"activity"},"2nd_place_medal":{keywords:["award","second"],char:'\u{1f948}',fitzpatrick_scale:!1,category:"activity"},"3rd_place_medal":{keywords:["award","third"],char:'\u{1f949}',fitzpatrick_scale:!1,category:"activity"},reminder_ribbon:{keywords:["sports","cause","support","awareness"],char:'\u{1f397}',fitzpatrick_scale:!1,category:"activity"},rosette:{keywords:["flower","decoration","military"],char:'\u{1f3f5}',fitzpatrick_scale:!1,category:"activity"},ticket:{keywords:["event","concert","pass"],char:'\u{1f3ab}',fitzpatrick_scale:!1,category:"activity"},tickets:{keywords:["sports","concert","entrance"],char:'\u{1f39f}',fitzpatrick_scale:!1,category:"activity"},performing_arts:{keywords:["acting","theater","drama"],char:'\u{1f3ad}',fitzpatrick_scale:!1,category:"activity"},art:{keywords:["design","paint","draw","colors"],char:'\u{1f3a8}',fitzpatrick_scale:!1,category:"activity"},circus_tent:{keywords:["festival","carnival","party"],char:'\u{1f3aa}',fitzpatrick_scale:!1,category:"activity"},woman_juggling:{keywords:["juggle","balance","skill","multitask"],char:'\u{1f939}\u200d\u2640\ufe0f',fitzpatrick_scale:!0,category:"activity"},man_juggling:{keywords:["juggle","balance","skill","multitask"],char:'\u{1f939}\u200d\u2642\ufe0f',fitzpatrick_scale:!0,category:"activity"},microphone:{keywords:["sound","music","PA","sing","talkshow"],char:'\u{1f3a4}',fitzpatrick_scale:!1,category:"activity"},headphones:{keywords:["music","score","gadgets"],char:'\u{1f3a7}',fitzpatrick_scale:!1,category:"activity"},musical_score:{keywords:["treble","clef","compose"],char:'\u{1f3bc}',fitzpatrick_scale:!1,category:"activity"},musical_keyboard:{keywords:["piano","instrument","compose"],char:'\u{1f3b9}',fitzpatrick_scale:!1,category:"activity"},drum:{keywords:["music","instrument","drumsticks","snare"],char:'\u{1f941}',fitzpatrick_scale:!1,category:"activity"},saxophone:{keywords:["music","instrument","jazz","blues"],char:'\u{1f3b7}',fitzpatrick_scale:!1,category:"activity"},trumpet:{keywords:["music","brass"],char:'\u{1f3ba}',fitzpatrick_scale:!1,category:"activity"},guitar:{keywords:["music","instrument"],char:'\u{1f3b8}',fitzpatrick_scale:!1,category:"activity"},violin:{keywords:["music","instrument","orchestra","symphony"],char:'\u{1f3bb}',fitzpatrick_scale:!1,category:"activity"},clapper:{keywords:["movie","film","record"],char:'\u{1f3ac}',fitzpatrick_scale:!1,category:"activity"},video_game:{keywords:["play","console","PS4","controller"],char:'\u{1f3ae}',fitzpatrick_scale:!1,category:"activity"},space_invader:{keywords:["game","arcade","play"],char:'\u{1f47e}',fitzpatrick_scale:!1,category:"activity"},dart:{keywords:["game","play","bar","target","bullseye"],char:'\u{1f3af}',fitzpatrick_scale:!1,category:"activity"},game_die:{keywords:["dice","random","tabletop","play","luck"],char:'\u{1f3b2}',fitzpatrick_scale:!1,category:"activity"},chess_pawn:{keywords:["expendable"],char:"\u265f",fitzpatrick_scale:!1,category:"activity"},slot_machine:{keywords:["bet","gamble","vegas","fruit machine","luck","casino"],char:'\u{1f3b0}',fitzpatrick_scale:!1,category:"activity"},jigsaw:{keywords:["interlocking","puzzle","piece"],char:'\u{1f9e9}',fitzpatrick_scale:!1,category:"activity"},bowling:{keywords:["sports","fun","play"],char:'\u{1f3b3}',fitzpatrick_scale:!1,category:"activity"},red_car:{keywords:["red","transportation","vehicle"],char:'\u{1f697}',fitzpatrick_scale:!1,category:"travel_and_places"},taxi:{keywords:["uber","vehicle","cars","transportation"],char:'\u{1f695}',fitzpatrick_scale:!1,category:"travel_and_places"},blue_car:{keywords:["transportation","vehicle"],char:'\u{1f699}',fitzpatrick_scale:!1,category:"travel_and_places"},bus:{keywords:["car","vehicle","transportation"],char:'\u{1f68c}',fitzpatrick_scale:!1,category:"travel_and_places"},trolleybus:{keywords:["bart","transportation","vehicle"],char:'\u{1f68e}',fitzpatrick_scale:!1,category:"travel_and_places"},racing_car:{keywords:["sports","race","fast","formula","f1"],char:'\u{1f3ce}',fitzpatrick_scale:!1,category:"travel_and_places"},police_car:{keywords:["vehicle","cars","transportation","law","legal","enforcement"],char:'\u{1f693}',fitzpatrick_scale:!1,category:"travel_and_places"},ambulance:{keywords:["health","911","hospital"],char:'\u{1f691}',fitzpatrick_scale:!1,category:"travel_and_places"},fire_engine:{keywords:["transportation","cars","vehicle"],char:'\u{1f692}',fitzpatrick_scale:!1,category:"travel_and_places"},minibus:{keywords:["vehicle","car","transportation"],char:'\u{1f690}',fitzpatrick_scale:!1,category:"travel_and_places"},truck:{keywords:["cars","transportation"],char:'\u{1f69a}',fitzpatrick_scale:!1,category:"travel_and_places"},articulated_lorry:{keywords:["vehicle","cars","transportation","express"],char:'\u{1f69b}',fitzpatrick_scale:!1,category:"travel_and_places"},tractor:{keywords:["vehicle","car","farming","agriculture"],char:'\u{1f69c}',fitzpatrick_scale:!1,category:"travel_and_places"},kick_scooter:{keywords:["vehicle","kick","razor"],char:'\u{1f6f4}',fitzpatrick_scale:!1,category:"travel_and_places"},motorcycle:{keywords:["race","sports","fast"],char:'\u{1f3cd}',fitzpatrick_scale:!1,category:"travel_and_places"},bike:{keywords:["sports","bicycle","exercise","hipster"],char:'\u{1f6b2}',fitzpatrick_scale:!1,category:"travel_and_places"},motor_scooter:{keywords:["vehicle","vespa","sasha"],char:'\u{1f6f5}',fitzpatrick_scale:!1,category:"travel_and_places"},rotating_light:{keywords:["police","ambulance","911","emergency","alert","error","pinged","law","legal"],char:'\u{1f6a8}',fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_police_car:{keywords:["vehicle","law","legal","enforcement","911"],char:'\u{1f694}',fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_bus:{keywords:["vehicle","transportation"],char:'\u{1f68d}',fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_automobile:{keywords:["car","vehicle","transportation"],char:'\u{1f698}',fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_taxi:{keywords:["vehicle","cars","uber"],char:'\u{1f696}',fitzpatrick_scale:!1,category:"travel_and_places"},aerial_tramway:{keywords:["transportation","vehicle","ski"],char:'\u{1f6a1}',fitzpatrick_scale:!1,category:"travel_and_places"},mountain_cableway:{keywords:["transportation","vehicle","ski"],char:'\u{1f6a0}',fitzpatrick_scale:!1,category:"travel_and_places"},suspension_railway:{keywords:["vehicle","transportation"],char:'\u{1f69f}',fitzpatrick_scale:!1,category:"travel_and_places"},railway_car:{keywords:["transportation","vehicle"],char:'\u{1f683}',fitzpatrick_scale:!1,category:"travel_and_places"},train:{keywords:["transportation","vehicle","carriage","public","travel"],char:'\u{1f68b}',fitzpatrick_scale:!1,category:"travel_and_places"},monorail:{keywords:["transportation","vehicle"],char:'\u{1f69d}',fitzpatrick_scale:!1,category:"travel_and_places"},bullettrain_side:{keywords:["transportation","vehicle"],char:'\u{1f684}',fitzpatrick_scale:!1,category:"travel_and_places"},bullettrain_front:{keywords:["transportation","vehicle","speed","fast","public","travel"],char:'\u{1f685}',fitzpatrick_scale:!1,category:"travel_and_places"},light_rail:{keywords:["transportation","vehicle"],char:'\u{1f688}',fitzpatrick_scale:!1,category:"travel_and_places"},mountain_railway:{keywords:["transportation","vehicle"],char:'\u{1f69e}',fitzpatrick_scale:!1,category:"travel_and_places"},steam_locomotive:{keywords:["transportation","vehicle","train"],char:'\u{1f682}',fitzpatrick_scale:!1,category:"travel_and_places"},train2:{keywords:["transportation","vehicle"],char:'\u{1f686}',fitzpatrick_scale:!1,category:"travel_and_places"},metro:{keywords:["transportation","blue-square","mrt","underground","tube"],char:'\u{1f687}',fitzpatrick_scale:!1,category:"travel_and_places"},tram:{keywords:["transportation","vehicle"],char:'\u{1f68a}',fitzpatrick_scale:!1,category:"travel_and_places"},station:{keywords:["transportation","vehicle","public"],char:'\u{1f689}',fitzpatrick_scale:!1,category:"travel_and_places"},flying_saucer:{keywords:["transportation","vehicle","ufo"],char:'\u{1f6f8}',fitzpatrick_scale:!1,category:"travel_and_places"},helicopter:{keywords:["transportation","vehicle","fly"],char:'\u{1f681}',fitzpatrick_scale:!1,category:"travel_and_places"},small_airplane:{keywords:["flight","transportation","fly","vehicle"],char:'\u{1f6e9}',fitzpatrick_scale:!1,category:"travel_and_places"},airplane:{keywords:["vehicle","transportation","flight","fly"],char:'\u2708\ufe0f',fitzpatrick_scale:!1,category:"travel_and_places"},flight_departure:{keywords:["airport","flight","landing"],char:'\u{1f6eb}',fitzpatrick_scale:!1,category:"travel_and_places"},flight_arrival:{keywords:["airport","flight","boarding"],char:'\u{1f6ec}',fitzpatrick_scale:!1,category:"travel_and_places"},sailboat:{keywords:["ship","summer","transportation","water","sailing"],char:'\u26f5',fitzpatrick_scale:!1,category:"travel_and_places"},motor_boat:{keywords:["ship"],char:'\u{1f6e5}',fitzpatrick_scale:!1,category:"travel_and_places"},speedboat:{keywords:["ship","transportation","vehicle","summer"],char:'\u{1f6a4}',fitzpatrick_scale:!1,category:"travel_and_places"},ferry:{keywords:["boat","ship","yacht"],char:'\u26f4',fitzpatrick_scale:!1,category:"travel_and_places"},passenger_ship:{keywords:["yacht","cruise","ferry"],char:'\u{1f6f3}',fitzpatrick_scale:!1,category:"travel_and_places"},rocket:{keywords:["launch","ship","staffmode","NASA","outer space","outer_space","fly"],char:'\u{1f680}',fitzpatrick_scale:!1,category:"travel_and_places"},artificial_satellite:{keywords:["communication","gps","orbit","spaceflight","NASA","ISS"],char:'\u{1f6f0}',fitzpatrick_scale:!1,category:"travel_and_places"},seat:{keywords:["sit","airplane","transport","bus","flight","fly"],char:'\u{1f4ba}',fitzpatrick_scale:!1,category:"travel_and_places"},canoe:{keywords:["boat","paddle","water","ship"],char:'\u{1f6f6}',fitzpatrick_scale:!1,category:"travel_and_places"},anchor:{keywords:["ship","ferry","sea","boat"],char:'\u2693',fitzpatrick_scale:!1,category:"travel_and_places"},construction:{keywords:["wip","progress","caution","warning"],char:'\u{1f6a7}',fitzpatrick_scale:!1,category:"travel_and_places"},fuelpump:{keywords:["gas station","petroleum"],char:'\u26fd',fitzpatrick_scale:!1,category:"travel_and_places"},busstop:{keywords:["transportation","wait"],char:'\u{1f68f}',fitzpatrick_scale:!1,category:"travel_and_places"},vertical_traffic_light:{keywords:["transportation","driving"],char:'\u{1f6a6}',fitzpatrick_scale:!1,category:"travel_and_places"},traffic_light:{keywords:["transportation","signal"],char:'\u{1f6a5}',fitzpatrick_scale:!1,category:"travel_and_places"},checkered_flag:{keywords:["contest","finishline","race","gokart"],char:'\u{1f3c1}',fitzpatrick_scale:!1,category:"travel_and_places"},ship:{keywords:["transportation","titanic","deploy"],char:'\u{1f6a2}',fitzpatrick_scale:!1,category:"travel_and_places"},ferris_wheel:{keywords:["photo","carnival","londoneye"],char:'\u{1f3a1}',fitzpatrick_scale:!1,category:"travel_and_places"},roller_coaster:{keywords:["carnival","playground","photo","fun"],char:'\u{1f3a2}',fitzpatrick_scale:!1,category:"travel_and_places"},carousel_horse:{keywords:["photo","carnival"],char:'\u{1f3a0}',fitzpatrick_scale:!1,category:"travel_and_places"},building_construction:{keywords:["wip","working","progress"],char:'\u{1f3d7}',fitzpatrick_scale:!1,category:"travel_and_places"},foggy:{keywords:["photo","mountain"],char:'\u{1f301}',fitzpatrick_scale:!1,category:"travel_and_places"},tokyo_tower:{keywords:["photo","japanese"],char:'\u{1f5fc}',fitzpatrick_scale:!1,category:"travel_and_places"},factory:{keywords:["building","industry","pollution","smoke"],char:'\u{1f3ed}',fitzpatrick_scale:!1,category:"travel_and_places"},fountain:{keywords:["photo","summer","water","fresh"],char:'\u26f2',fitzpatrick_scale:!1,category:"travel_and_places"},rice_scene:{keywords:["photo","japan","asia","tsukimi"],char:'\u{1f391}',fitzpatrick_scale:!1,category:"travel_and_places"},mountain:{keywords:["photo","nature","environment"],char:'\u26f0',fitzpatrick_scale:!1,category:"travel_and_places"},mountain_snow:{keywords:["photo","nature","environment","winter","cold"],char:'\u{1f3d4}',fitzpatrick_scale:!1,category:"travel_and_places"},mount_fuji:{keywords:["photo","mountain","nature","japanese"],char:'\u{1f5fb}',fitzpatrick_scale:!1,category:"travel_and_places"},volcano:{keywords:["photo","nature","disaster"],char:'\u{1f30b}',fitzpatrick_scale:!1,category:"travel_and_places"},japan:{keywords:["nation","country","japanese","asia"],char:'\u{1f5fe}',fitzpatrick_scale:!1,category:"travel_and_places"},camping:{keywords:["photo","outdoors","tent"],char:'\u{1f3d5}',fitzpatrick_scale:!1,category:"travel_and_places"},tent:{keywords:["photo","camping","outdoors"],char:'\u26fa',fitzpatrick_scale:!1,category:"travel_and_places"},national_park:{keywords:["photo","environment","nature"],char:'\u{1f3de}',fitzpatrick_scale:!1,category:"travel_and_places"},motorway:{keywords:["road","cupertino","interstate","highway"],char:'\u{1f6e3}',fitzpatrick_scale:!1,category:"travel_and_places"},railway_track:{keywords:["train","transportation"],char:'\u{1f6e4}',fitzpatrick_scale:!1,category:"travel_and_places"},sunrise:{keywords:["morning","view","vacation","photo"],char:'\u{1f305}',fitzpatrick_scale:!1,category:"travel_and_places"},sunrise_over_mountains:{keywords:["view","vacation","photo"],char:'\u{1f304}',fitzpatrick_scale:!1,category:"travel_and_places"},desert:{keywords:["photo","warm","saharah"],char:'\u{1f3dc}',fitzpatrick_scale:!1,category:"travel_and_places"},beach_umbrella:{keywords:["weather","summer","sunny","sand","mojito"],char:'\u{1f3d6}',fitzpatrick_scale:!1,category:"travel_and_places"},desert_island:{keywords:["photo","tropical","mojito"],char:'\u{1f3dd}',fitzpatrick_scale:!1,category:"travel_and_places"},city_sunrise:{keywords:["photo","good morning","dawn"],char:'\u{1f307}',fitzpatrick_scale:!1,category:"travel_and_places"},city_sunset:{keywords:["photo","evening","sky","buildings"],char:'\u{1f306}',fitzpatrick_scale:!1,category:"travel_and_places"},cityscape:{keywords:["photo","night life","urban"],char:'\u{1f3d9}',fitzpatrick_scale:!1,category:"travel_and_places"},night_with_stars:{keywords:["evening","city","downtown"],char:'\u{1f303}',fitzpatrick_scale:!1,category:"travel_and_places"},bridge_at_night:{keywords:["photo","sanfrancisco"],char:'\u{1f309}',fitzpatrick_scale:!1,category:"travel_and_places"},milky_way:{keywords:["photo","space","stars"],char:'\u{1f30c}',fitzpatrick_scale:!1,category:"travel_and_places"},stars:{keywords:["night","photo"],char:'\u{1f320}',fitzpatrick_scale:!1,category:"travel_and_places"},sparkler:{keywords:["stars","night","shine"],char:'\u{1f387}',fitzpatrick_scale:!1,category:"travel_and_places"},fireworks:{keywords:["photo","festival","carnival","congratulations"],char:'\u{1f386}',fitzpatrick_scale:!1,category:"travel_and_places"},rainbow:{keywords:["nature","happy","unicorn_face","photo","sky","spring"],char:'\u{1f308}',fitzpatrick_scale:!1,category:"travel_and_places"},houses:{keywords:["buildings","photo"],char:'\u{1f3d8}',fitzpatrick_scale:!1,category:"travel_and_places"},european_castle:{keywords:["building","royalty","history"],char:'\u{1f3f0}',fitzpatrick_scale:!1,category:"travel_and_places"},japanese_castle:{keywords:["photo","building"],char:'\u{1f3ef}',fitzpatrick_scale:!1,category:"travel_and_places"},stadium:{keywords:["photo","place","sports","concert","venue"],char:'\u{1f3df}',fitzpatrick_scale:!1,category:"travel_and_places"},statue_of_liberty:{keywords:["american","newyork"],char:'\u{1f5fd}',fitzpatrick_scale:!1,category:"travel_and_places"},house:{keywords:["building","home"],char:'\u{1f3e0}',fitzpatrick_scale:!1,category:"travel_and_places"},house_with_garden:{keywords:["home","plant","nature"],char:'\u{1f3e1}',fitzpatrick_scale:!1,category:"travel_and_places"},derelict_house:{keywords:["abandon","evict","broken","building"],char:'\u{1f3da}',fitzpatrick_scale:!1,category:"travel_and_places"},office:{keywords:["building","bureau","work"],char:'\u{1f3e2}',fitzpatrick_scale:!1,category:"travel_and_places"},department_store:{keywords:["building","shopping","mall"],char:'\u{1f3ec}',fitzpatrick_scale:!1,category:"travel_and_places"},post_office:{keywords:["building","envelope","communication"],char:'\u{1f3e3}',fitzpatrick_scale:!1,category:"travel_and_places"},european_post_office:{keywords:["building","email"],char:'\u{1f3e4}',fitzpatrick_scale:!1,category:"travel_and_places"},hospital:{keywords:["building","health","surgery","doctor"],char:'\u{1f3e5}',fitzpatrick_scale:!1,category:"travel_and_places"},bank:{keywords:["building","money","sales","cash","business","enterprise"],char:'\u{1f3e6}',fitzpatrick_scale:!1,category:"travel_and_places"},hotel:{keywords:["building","accomodation","checkin"],char:'\u{1f3e8}',fitzpatrick_scale:!1,category:"travel_and_places"},convenience_store:{keywords:["building","shopping","groceries"],char:'\u{1f3ea}',fitzpatrick_scale:!1,category:"travel_and_places"},school:{keywords:["building","student","education","learn","teach"],char:'\u{1f3eb}',fitzpatrick_scale:!1,category:"travel_and_places"},love_hotel:{keywords:["like","affection","dating"],char:'\u{1f3e9}',fitzpatrick_scale:!1,category:"travel_and_places"},wedding:{keywords:["love","like","affection","couple","marriage","bride","groom"],char:'\u{1f492}',fitzpatrick_scale:!1,category:"travel_and_places"},classical_building:{keywords:["art","culture","history"],char:'\u{1f3db}',fitzpatrick_scale:!1,category:"travel_and_places"},church:{keywords:["building","religion","christ"],char:'\u26ea',fitzpatrick_scale:!1,category:"travel_and_places"},mosque:{keywords:["islam","worship","minaret"],char:'\u{1f54c}',fitzpatrick_scale:!1,category:"travel_and_places"},synagogue:{keywords:["judaism","worship","temple","jewish"],char:'\u{1f54d}',fitzpatrick_scale:!1,category:"travel_and_places"},kaaba:{keywords:["mecca","mosque","islam"],char:'\u{1f54b}',fitzpatrick_scale:!1,category:"travel_and_places"},shinto_shrine:{keywords:["temple","japan","kyoto"],char:'\u26e9',fitzpatrick_scale:!1,category:"travel_and_places"},watch:{keywords:["time","accessories"],char:'\u231a',fitzpatrick_scale:!1,category:"objects"},iphone:{keywords:["technology","apple","gadgets","dial"],char:'\u{1f4f1}',fitzpatrick_scale:!1,category:"objects"},calling:{keywords:["iphone","incoming"],char:'\u{1f4f2}',fitzpatrick_scale:!1,category:"objects"},computer:{keywords:["technology","laptop","screen","display","monitor"],char:'\u{1f4bb}',fitzpatrick_scale:!1,category:"objects"},keyboard:{keywords:["technology","computer","type","input","text"],char:'\u2328',fitzpatrick_scale:!1,category:"objects"},desktop_computer:{keywords:["technology","computing","screen"],char:'\u{1f5a5}',fitzpatrick_scale:!1,category:"objects"},printer:{keywords:["paper","ink"],char:'\u{1f5a8}',fitzpatrick_scale:!1,category:"objects"},computer_mouse:{keywords:["click"],char:'\u{1f5b1}',fitzpatrick_scale:!1,category:"objects"},trackball:{keywords:["technology","trackpad"],char:'\u{1f5b2}',fitzpatrick_scale:!1,category:"objects"},joystick:{keywords:["game","play"],char:'\u{1f579}',fitzpatrick_scale:!1,category:"objects"},clamp:{keywords:["tool"],char:'\u{1f5dc}',fitzpatrick_scale:!1,category:"objects"},minidisc:{keywords:["technology","record","data","disk","90s"],char:'\u{1f4bd}',fitzpatrick_scale:!1,category:"objects"},floppy_disk:{keywords:["oldschool","technology","save","90s","80s"],char:'\u{1f4be}',fitzpatrick_scale:!1,category:"objects"},cd:{keywords:["technology","dvd","disk","disc","90s"],char:'\u{1f4bf}',fitzpatrick_scale:!1,category:"objects"},dvd:{keywords:["cd","disk","disc"],char:'\u{1f4c0}',fitzpatrick_scale:!1,category:"objects"},vhs:{keywords:["record","video","oldschool","90s","80s"],char:'\u{1f4fc}',fitzpatrick_scale:!1,category:"objects"},camera:{keywords:["gadgets","photography"],char:'\u{1f4f7}',fitzpatrick_scale:!1,category:"objects"},camera_flash:{keywords:["photography","gadgets"],char:'\u{1f4f8}',fitzpatrick_scale:!1,category:"objects"},video_camera:{keywords:["film","record"],char:'\u{1f4f9}',fitzpatrick_scale:!1,category:"objects"},movie_camera:{keywords:["film","record"],char:'\u{1f3a5}',fitzpatrick_scale:!1,category:"objects"},film_projector:{keywords:["video","tape","record","movie"],char:'\u{1f4fd}',fitzpatrick_scale:!1,category:"objects"},film_strip:{keywords:["movie"],char:'\u{1f39e}',fitzpatrick_scale:!1,category:"objects"},telephone_receiver:{keywords:["technology","communication","dial"],char:'\u{1f4de}',fitzpatrick_scale:!1,category:"objects"},phone:{keywords:["technology","communication","dial","telephone"],char:'\u260e\ufe0f',fitzpatrick_scale:!1,category:"objects"},pager:{keywords:["bbcall","oldschool","90s"],char:'\u{1f4df}',fitzpatrick_scale:!1,category:"objects"},fax:{keywords:["communication","technology"],char:'\u{1f4e0}',fitzpatrick_scale:!1,category:"objects"},tv:{keywords:["technology","program","oldschool","show","television"],char:'\u{1f4fa}',fitzpatrick_scale:!1,category:"objects"},radio:{keywords:["communication","music","podcast","program"],char:'\u{1f4fb}',fitzpatrick_scale:!1,category:"objects"},studio_microphone:{keywords:["sing","recording","artist","talkshow"],char:'\u{1f399}',fitzpatrick_scale:!1,category:"objects"},level_slider:{keywords:["scale"],char:'\u{1f39a}',fitzpatrick_scale:!1,category:"objects"},control_knobs:{keywords:["dial"],char:'\u{1f39b}',fitzpatrick_scale:!1,category:"objects"},compass:{keywords:["magnetic","navigation","orienteering"],char:'\u{1f9ed}',fitzpatrick_scale:!1,category:"objects"},stopwatch:{keywords:["time","deadline"],char:'\u23f1',fitzpatrick_scale:!1,category:"objects"},timer_clock:{keywords:["alarm"],char:'\u23f2',fitzpatrick_scale:!1,category:"objects"},alarm_clock:{keywords:["time","wake"],char:'\u23f0',fitzpatrick_scale:!1,category:"objects"},mantelpiece_clock:{keywords:["time"],char:'\u{1f570}',fitzpatrick_scale:!1,category:"objects"},hourglass_flowing_sand:{keywords:["oldschool","time","countdown"],char:'\u23f3',fitzpatrick_scale:!1,category:"objects"},hourglass:{keywords:["time","clock","oldschool","limit","exam","quiz","test"],char:'\u231b',fitzpatrick_scale:!1,category:"objects"},satellite:{keywords:["communication","future","radio","space"],char:'\u{1f4e1}',fitzpatrick_scale:!1,category:"objects"},battery:{keywords:["power","energy","sustain"],char:'\u{1f50b}',fitzpatrick_scale:!1,category:"objects"},electric_plug:{keywords:["charger","power"],char:'\u{1f50c}',fitzpatrick_scale:!1,category:"objects"},bulb:{keywords:["light","electricity","idea"],char:'\u{1f4a1}',fitzpatrick_scale:!1,category:"objects"},flashlight:{keywords:["dark","camping","sight","night"],char:'\u{1f526}',fitzpatrick_scale:!1,category:"objects"},candle:{keywords:["fire","wax"],char:'\u{1f56f}',fitzpatrick_scale:!1,category:"objects"},fire_extinguisher:{keywords:["quench"],char:'\u{1f9ef}',fitzpatrick_scale:!1,category:"objects"},wastebasket:{keywords:["bin","trash","rubbish","garbage","toss"],char:'\u{1f5d1}',fitzpatrick_scale:!1,category:"objects"},oil_drum:{keywords:["barrell"],char:'\u{1f6e2}',fitzpatrick_scale:!1,category:"objects"},money_with_wings:{keywords:["dollar","bills","payment","sale"],char:'\u{1f4b8}',fitzpatrick_scale:!1,category:"objects"},dollar:{keywords:["money","sales","bill","currency"],char:'\u{1f4b5}',fitzpatrick_scale:!1,category:"objects"},yen:{keywords:["money","sales","japanese","dollar","currency"],char:'\u{1f4b4}',fitzpatrick_scale:!1,category:"objects"},euro:{keywords:["money","sales","dollar","currency"],char:'\u{1f4b6}',fitzpatrick_scale:!1,category:"objects"},pound:{keywords:["british","sterling","money","sales","bills","uk","england","currency"],char:'\u{1f4b7}',fitzpatrick_scale:!1,category:"objects"},moneybag:{keywords:["dollar","payment","coins","sale"],char:'\u{1f4b0}',fitzpatrick_scale:!1,category:"objects"},credit_card:{keywords:["money","sales","dollar","bill","payment","shopping"],char:'\u{1f4b3}',fitzpatrick_scale:!1,category:"objects"},gem:{keywords:["blue","ruby","diamond","jewelry"],char:'\u{1f48e}',fitzpatrick_scale:!1,category:"objects"},balance_scale:{keywords:["law","fairness","weight"],char:'\u2696',fitzpatrick_scale:!1,category:"objects"},toolbox:{keywords:["tools","diy","fix","maintainer","mechanic"],char:'\u{1f9f0}',fitzpatrick_scale:!1,category:"objects"},wrench:{keywords:["tools","diy","ikea","fix","maintainer"],char:'\u{1f527}',fitzpatrick_scale:!1,category:"objects"},hammer:{keywords:["tools","build","create"],char:'\u{1f528}',fitzpatrick_scale:!1,category:"objects"},hammer_and_pick:{keywords:["tools","build","create"],char:'\u2692',fitzpatrick_scale:!1,category:"objects"},hammer_and_wrench:{keywords:["tools","build","create"],char:'\u{1f6e0}',fitzpatrick_scale:!1,category:"objects"},pick:{keywords:["tools","dig"],char:'\u26cf',fitzpatrick_scale:!1,category:"objects"},nut_and_bolt:{keywords:["handy","tools","fix"],char:'\u{1f529}',fitzpatrick_scale:!1,category:"objects"},gear:{keywords:["cog"],char:'\u2699',fitzpatrick_scale:!1,category:"objects"},brick:{keywords:["bricks"],char:'\u{1f9f1}',fitzpatrick_scale:!1,category:"objects"},chains:{keywords:["lock","arrest"],char:'\u26d3',fitzpatrick_scale:!1,category:"objects"},magnet:{keywords:["attraction","magnetic"],char:'\u{1f9f2}',fitzpatrick_scale:!1,category:"objects"},gun:{keywords:["violence","weapon","pistol","revolver"],char:'\u{1f52b}',fitzpatrick_scale:!1,category:"objects"},bomb:{keywords:["boom","explode","explosion","terrorism"],char:'\u{1f4a3}',fitzpatrick_scale:!1,category:"objects"},firecracker:{keywords:["dynamite","boom","explode","explosion","explosive"],char:'\u{1f9e8}',fitzpatrick_scale:!1,category:"objects"},hocho:{keywords:["knife","blade","cutlery","kitchen","weapon"],char:'\u{1f52a}',fitzpatrick_scale:!1,category:"objects"},dagger:{keywords:["weapon"],char:'\u{1f5e1}',fitzpatrick_scale:!1,category:"objects"},crossed_swords:{keywords:["weapon"],char:'\u2694',fitzpatrick_scale:!1,category:"objects"},shield:{keywords:["protection","security"],char:'\u{1f6e1}',fitzpatrick_scale:!1,category:"objects"},smoking:{keywords:["kills","tobacco","cigarette","joint","smoke"],char:'\u{1f6ac}',fitzpatrick_scale:!1,category:"objects"},skull_and_crossbones:{keywords:["poison","danger","deadly","scary","death","pirate","evil"],char:'\u2620',fitzpatrick_scale:!1,category:"objects"},coffin:{keywords:["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],char:'\u26b0',fitzpatrick_scale:!1,category:"objects"},funeral_urn:{keywords:["dead","die","death","rip","ashes"],char:'\u26b1',fitzpatrick_scale:!1,category:"objects"},amphora:{keywords:["vase","jar"],char:'\u{1f3fa}',fitzpatrick_scale:!1,category:"objects"},crystal_ball:{keywords:["disco","party","magic","circus","fortune_teller"],char:'\u{1f52e}',fitzpatrick_scale:!1,category:"objects"},prayer_beads:{keywords:["dhikr","religious"],char:'\u{1f4ff}',fitzpatrick_scale:!1,category:"objects"},nazar_amulet:{keywords:["bead","charm"],char:'\u{1f9ff}',fitzpatrick_scale:!1,category:"objects"},barber:{keywords:["hair","salon","style"],char:'\u{1f488}',fitzpatrick_scale:!1,category:"objects"},alembic:{keywords:["distilling","science","experiment","chemistry"],char:'\u2697',fitzpatrick_scale:!1,category:"objects"},telescope:{keywords:["stars","space","zoom","science","astronomy"],char:'\u{1f52d}',fitzpatrick_scale:!1,category:"objects"},microscope:{keywords:["laboratory","experiment","zoomin","science","study"],char:'\u{1f52c}',fitzpatrick_scale:!1,category:"objects"},hole:{keywords:["embarrassing"],char:'\u{1f573}',fitzpatrick_scale:!1,category:"objects"},pill:{keywords:["health","medicine","doctor","pharmacy","drug"],char:'\u{1f48a}',fitzpatrick_scale:!1,category:"objects"},syringe:{keywords:["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],char:'\u{1f489}',fitzpatrick_scale:!1,category:"objects"},dna:{keywords:["biologist","genetics","life"],char:'\u{1f9ec}',fitzpatrick_scale:!1,category:"objects"},microbe:{keywords:["amoeba","bacteria","germs"],char:'\u{1f9a0}',fitzpatrick_scale:!1,category:"objects"},petri_dish:{keywords:["bacteria","biology","culture","lab"],char:'\u{1f9eb}',fitzpatrick_scale:!1,category:"objects"},test_tube:{keywords:["chemistry","experiment","lab","science"],char:'\u{1f9ea}',fitzpatrick_scale:!1,category:"objects"},thermometer:{keywords:["weather","temperature","hot","cold"],char:'\u{1f321}',fitzpatrick_scale:!1,category:"objects"},broom:{keywords:["cleaning","sweeping","witch"],char:'\u{1f9f9}',fitzpatrick_scale:!1,category:"objects"},basket:{keywords:["laundry"],char:'\u{1f9fa}',fitzpatrick_scale:!1,category:"objects"},toilet_paper:{keywords:["roll"],char:'\u{1f9fb}',fitzpatrick_scale:!1,category:"objects"},label:{keywords:["sale","tag"],char:'\u{1f3f7}',fitzpatrick_scale:!1,category:"objects"},bookmark:{keywords:["favorite","label","save"],char:'\u{1f516}',fitzpatrick_scale:!1,category:"objects"},toilet:{keywords:["restroom","wc","washroom","bathroom","potty"],char:'\u{1f6bd}',fitzpatrick_scale:!1,category:"objects"},shower:{keywords:["clean","water","bathroom"],char:'\u{1f6bf}',fitzpatrick_scale:!1,category:"objects"},bathtub:{keywords:["clean","shower","bathroom"],char:'\u{1f6c1}',fitzpatrick_scale:!1,category:"objects"},soap:{keywords:["bar","bathing","cleaning","lather"],char:'\u{1f9fc}',fitzpatrick_scale:!1,category:"objects"},sponge:{keywords:["absorbing","cleaning","porous"],char:'\u{1f9fd}',fitzpatrick_scale:!1,category:"objects"},lotion_bottle:{keywords:["moisturizer","sunscreen"],char:'\u{1f9f4}',fitzpatrick_scale:!1,category:"objects"},key:{keywords:["lock","door","password"],char:'\u{1f511}',fitzpatrick_scale:!1,category:"objects"},old_key:{keywords:["lock","door","password"],char:'\u{1f5dd}',fitzpatrick_scale:!1,category:"objects"},couch_and_lamp:{keywords:["read","chill"],char:'\u{1f6cb}',fitzpatrick_scale:!1,category:"objects"},sleeping_bed:{keywords:["bed","rest"],char:'\u{1f6cc}',fitzpatrick_scale:!0,category:"objects"},bed:{keywords:["sleep","rest"],char:'\u{1f6cf}',fitzpatrick_scale:!1,category:"objects"},door:{keywords:["house","entry","exit"],char:'\u{1f6aa}',fitzpatrick_scale:!1,category:"objects"},bellhop_bell:{keywords:["service"],char:'\u{1f6ce}',fitzpatrick_scale:!1,category:"objects"},teddy_bear:{keywords:["plush","stuffed"],char:'\u{1f9f8}',fitzpatrick_scale:!1,category:"objects"},framed_picture:{keywords:["photography"],char:'\u{1f5bc}',fitzpatrick_scale:!1,category:"objects"},world_map:{keywords:["location","direction"],char:'\u{1f5fa}',fitzpatrick_scale:!1,category:"objects"},parasol_on_ground:{keywords:["weather","summer"],char:'\u26f1',fitzpatrick_scale:!1,category:"objects"},moyai:{keywords:["rock","easter island","moai"],char:'\u{1f5ff}',fitzpatrick_scale:!1,category:"objects"},shopping:{keywords:["mall","buy","purchase"],char:'\u{1f6cd}',fitzpatrick_scale:!1,category:"objects"},shopping_cart:{keywords:["trolley"],char:'\u{1f6d2}',fitzpatrick_scale:!1,category:"objects"},balloon:{keywords:["party","celebration","birthday","circus"],char:'\u{1f388}',fitzpatrick_scale:!1,category:"objects"},flags:{keywords:["fish","japanese","koinobori","carp","banner"],char:'\u{1f38f}',fitzpatrick_scale:!1,category:"objects"},ribbon:{keywords:["decoration","pink","girl","bowtie"],char:'\u{1f380}',fitzpatrick_scale:!1,category:"objects"},gift:{keywords:["present","birthday","christmas","xmas"],char:'\u{1f381}',fitzpatrick_scale:!1,category:"objects"},confetti_ball:{keywords:["festival","party","birthday","circus"],char:'\u{1f38a}',fitzpatrick_scale:!1,category:"objects"},tada:{keywords:["party","congratulations","birthday","magic","circus","celebration"],char:'\u{1f389}',fitzpatrick_scale:!1,category:"objects"},dolls:{keywords:["japanese","toy","kimono"],char:'\u{1f38e}',fitzpatrick_scale:!1,category:"objects"},wind_chime:{keywords:["nature","ding","spring","bell"],char:'\u{1f390}',fitzpatrick_scale:!1,category:"objects"},crossed_flags:{keywords:["japanese","nation","country","border"],char:'\u{1f38c}',fitzpatrick_scale:!1,category:"objects"},izakaya_lantern:{keywords:["light","paper","halloween","spooky"],char:'\u{1f3ee}',fitzpatrick_scale:!1,category:"objects"},red_envelope:{keywords:["gift"],char:'\u{1f9e7}',fitzpatrick_scale:!1,category:"objects"},email:{keywords:["letter","postal","inbox","communication"],char:'\u2709\ufe0f',fitzpatrick_scale:!1,category:"objects"},envelope_with_arrow:{keywords:["email","communication"],char:'\u{1f4e9}',fitzpatrick_scale:!1,category:"objects"},incoming_envelope:{keywords:["email","inbox"],char:'\u{1f4e8}',fitzpatrick_scale:!1,category:"objects"},"e-mail":{keywords:["communication","inbox"],char:'\u{1f4e7}',fitzpatrick_scale:!1,category:"objects"},love_letter:{keywords:["email","like","affection","envelope","valentines"],char:'\u{1f48c}',fitzpatrick_scale:!1,category:"objects"},postbox:{keywords:["email","letter","envelope"],char:'\u{1f4ee}',fitzpatrick_scale:!1,category:"objects"},mailbox_closed:{keywords:["email","communication","inbox"],char:'\u{1f4ea}',fitzpatrick_scale:!1,category:"objects"},mailbox:{keywords:["email","inbox","communication"],char:'\u{1f4eb}',fitzpatrick_scale:!1,category:"objects"},mailbox_with_mail:{keywords:["email","inbox","communication"],char:'\u{1f4ec}',fitzpatrick_scale:!1,category:"objects"},mailbox_with_no_mail:{keywords:["email","inbox"],char:'\u{1f4ed}',fitzpatrick_scale:!1,category:"objects"},package:{keywords:["mail","gift","cardboard","box","moving"],char:'\u{1f4e6}',fitzpatrick_scale:!1,category:"objects"},postal_horn:{keywords:["instrument","music"],char:'\u{1f4ef}',fitzpatrick_scale:!1,category:"objects"},inbox_tray:{keywords:["email","documents"],char:'\u{1f4e5}',fitzpatrick_scale:!1,category:"objects"},outbox_tray:{keywords:["inbox","email"],char:'\u{1f4e4}',fitzpatrick_scale:!1,category:"objects"},scroll:{keywords:["documents","ancient","history","paper"],char:'\u{1f4dc}',fitzpatrick_scale:!1,category:"objects"},page_with_curl:{keywords:["documents","office","paper"],char:'\u{1f4c3}',fitzpatrick_scale:!1,category:"objects"},bookmark_tabs:{keywords:["favorite","save","order","tidy"],char:'\u{1f4d1}',fitzpatrick_scale:!1,category:"objects"},receipt:{keywords:["accounting","expenses"],char:'\u{1f9fe}',fitzpatrick_scale:!1,category:"objects"},bar_chart:{keywords:["graph","presentation","stats"],char:'\u{1f4ca}',fitzpatrick_scale:!1,category:"objects"},chart_with_upwards_trend:{keywords:["graph","presentation","stats","recovery","business","economics","money","sales","good","success"],char:'\u{1f4c8}',fitzpatrick_scale:!1,category:"objects"},chart_with_downwards_trend:{keywords:["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],char:'\u{1f4c9}',fitzpatrick_scale:!1,category:"objects"},page_facing_up:{keywords:["documents","office","paper","information"],char:'\u{1f4c4}',fitzpatrick_scale:!1,category:"objects"},date:{keywords:["calendar","schedule"],char:'\u{1f4c5}',fitzpatrick_scale:!1,category:"objects"},calendar:{keywords:["schedule","date","planning"],char:'\u{1f4c6}',fitzpatrick_scale:!1,category:"objects"},spiral_calendar:{keywords:["date","schedule","planning"],char:'\u{1f5d3}',fitzpatrick_scale:!1,category:"objects"},card_index:{keywords:["business","stationery"],char:'\u{1f4c7}',fitzpatrick_scale:!1,category:"objects"},card_file_box:{keywords:["business","stationery"],char:'\u{1f5c3}',fitzpatrick_scale:!1,category:"objects"},ballot_box:{keywords:["election","vote"],char:'\u{1f5f3}',fitzpatrick_scale:!1,category:"objects"},file_cabinet:{keywords:["filing","organizing"],char:'\u{1f5c4}',fitzpatrick_scale:!1,category:"objects"},clipboard:{keywords:["stationery","documents"],char:'\u{1f4cb}',fitzpatrick_scale:!1,category:"objects"},spiral_notepad:{keywords:["memo","stationery"],char:'\u{1f5d2}',fitzpatrick_scale:!1,category:"objects"},file_folder:{keywords:["documents","business","office"],char:'\u{1f4c1}',fitzpatrick_scale:!1,category:"objects"},open_file_folder:{keywords:["documents","load"],char:'\u{1f4c2}',fitzpatrick_scale:!1,category:"objects"},card_index_dividers:{keywords:["organizing","business","stationery"],char:'\u{1f5c2}',fitzpatrick_scale:!1,category:"objects"},newspaper_roll:{keywords:["press","headline"],char:'\u{1f5de}',fitzpatrick_scale:!1,category:"objects"},newspaper:{keywords:["press","headline"],char:'\u{1f4f0}',fitzpatrick_scale:!1,category:"objects"},notebook:{keywords:["stationery","record","notes","paper","study"],char:'\u{1f4d3}',fitzpatrick_scale:!1,category:"objects"},closed_book:{keywords:["read","library","knowledge","textbook","learn"],char:'\u{1f4d5}',fitzpatrick_scale:!1,category:"objects"},green_book:{keywords:["read","library","knowledge","study"],char:'\u{1f4d7}',fitzpatrick_scale:!1,category:"objects"},blue_book:{keywords:["read","library","knowledge","learn","study"],char:'\u{1f4d8}',fitzpatrick_scale:!1,category:"objects"},orange_book:{keywords:["read","library","knowledge","textbook","study"],char:'\u{1f4d9}',fitzpatrick_scale:!1,category:"objects"},notebook_with_decorative_cover:{keywords:["classroom","notes","record","paper","study"],char:'\u{1f4d4}',fitzpatrick_scale:!1,category:"objects"},ledger:{keywords:["notes","paper"],char:'\u{1f4d2}',fitzpatrick_scale:!1,category:"objects"},books:{keywords:["literature","library","study"],char:'\u{1f4da}',fitzpatrick_scale:!1,category:"objects"},open_book:{keywords:["book","read","library","knowledge","literature","learn","study"],char:'\u{1f4d6}',fitzpatrick_scale:!1,category:"objects"},safety_pin:{keywords:["diaper"],char:'\u{1f9f7}',fitzpatrick_scale:!1,category:"objects"},link:{keywords:["rings","url"],char:'\u{1f517}',fitzpatrick_scale:!1,category:"objects"},paperclip:{keywords:["documents","stationery"],char:'\u{1f4ce}',fitzpatrick_scale:!1,category:"objects"},paperclips:{keywords:["documents","stationery"],char:'\u{1f587}',fitzpatrick_scale:!1,category:"objects"},scissors:{keywords:["stationery","cut"],char:'\u2702\ufe0f',fitzpatrick_scale:!1,category:"objects"},triangular_ruler:{keywords:["stationery","math","architect","sketch"],char:'\u{1f4d0}',fitzpatrick_scale:!1,category:"objects"},straight_ruler:{keywords:["stationery","calculate","length","math","school","drawing","architect","sketch"],char:'\u{1f4cf}',fitzpatrick_scale:!1,category:"objects"},abacus:{keywords:["calculation"],char:'\u{1f9ee}',fitzpatrick_scale:!1,category:"objects"},pushpin:{keywords:["stationery","mark","here"],char:'\u{1f4cc}',fitzpatrick_scale:!1,category:"objects"},round_pushpin:{keywords:["stationery","location","map","here"],char:'\u{1f4cd}',fitzpatrick_scale:!1,category:"objects"},triangular_flag_on_post:{keywords:["mark","milestone","place"],char:'\u{1f6a9}',fitzpatrick_scale:!1,category:"objects"},white_flag:{keywords:["losing","loser","lost","surrender","give up","fail"],char:'\u{1f3f3}',fitzpatrick_scale:!1,category:"objects"},black_flag:{keywords:["pirate"],char:'\u{1f3f4}',fitzpatrick_scale:!1,category:"objects"},rainbow_flag:{keywords:["flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"],char:'\u{1f3f3}\ufe0f\u200d\u{1f308}',fitzpatrick_scale:!1,category:"objects"},closed_lock_with_key:{keywords:["security","privacy"],char:'\u{1f510}',fitzpatrick_scale:!1,category:"objects"},lock:{keywords:["security","password","padlock"],char:'\u{1f512}',fitzpatrick_scale:!1,category:"objects"},unlock:{keywords:["privacy","security"],char:'\u{1f513}',fitzpatrick_scale:!1,category:"objects"},lock_with_ink_pen:{keywords:["security","secret"],char:'\u{1f50f}',fitzpatrick_scale:!1,category:"objects"},pen:{keywords:["stationery","writing","write"],char:'\u{1f58a}',fitzpatrick_scale:!1,category:"objects"},fountain_pen:{keywords:["stationery","writing","write"],char:'\u{1f58b}',fitzpatrick_scale:!1,category:"objects"},black_nib:{keywords:["pen","stationery","writing","write"],char:'\u2712\ufe0f',fitzpatrick_scale:!1,category:"objects"},memo:{keywords:["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"],char:'\u{1f4dd}',fitzpatrick_scale:!1,category:"objects"},pencil2:{keywords:["stationery","write","paper","writing","school","study"],char:'\u270f\ufe0f',fitzpatrick_scale:!1,category:"objects"},crayon:{keywords:["drawing","creativity"],char:'\u{1f58d}',fitzpatrick_scale:!1,category:"objects"},paintbrush:{keywords:["drawing","creativity","art"],char:'\u{1f58c}',fitzpatrick_scale:!1,category:"objects"},mag:{keywords:["search","zoom","find","detective"],char:'\u{1f50d}',fitzpatrick_scale:!1,category:"objects"},mag_right:{keywords:["search","zoom","find","detective"],char:'\u{1f50e}',fitzpatrick_scale:!1,category:"objects"},heart:{keywords:["love","like","valentines"],char:'\u2764\ufe0f',fitzpatrick_scale:!1,category:"symbols"},orange_heart:{keywords:["love","like","affection","valentines"],char:'\u{1f9e1}',fitzpatrick_scale:!1,category:"symbols"},yellow_heart:{keywords:["love","like","affection","valentines"],char:'\u{1f49b}',fitzpatrick_scale:!1,category:"symbols"},green_heart:{keywords:["love","like","affection","valentines"],char:'\u{1f49a}',fitzpatrick_scale:!1,category:"symbols"},blue_heart:{keywords:["love","like","affection","valentines"],char:'\u{1f499}',fitzpatrick_scale:!1,category:"symbols"},purple_heart:{keywords:["love","like","affection","valentines"],char:'\u{1f49c}',fitzpatrick_scale:!1,category:"symbols"},black_heart:{keywords:["evil"],char:'\u{1f5a4}',fitzpatrick_scale:!1,category:"symbols"},broken_heart:{keywords:["sad","sorry","break","heart","heartbreak"],char:'\u{1f494}',fitzpatrick_scale:!1,category:"symbols"},heavy_heart_exclamation:{keywords:["decoration","love"],char:'\u2763',fitzpatrick_scale:!1,category:"symbols"},two_hearts:{keywords:["love","like","affection","valentines","heart"],char:'\u{1f495}',fitzpatrick_scale:!1,category:"symbols"},revolving_hearts:{keywords:["love","like","affection","valentines"],char:'\u{1f49e}',fitzpatrick_scale:!1,category:"symbols"},heartbeat:{keywords:["love","like","affection","valentines","pink","heart"],char:'\u{1f493}',fitzpatrick_scale:!1,category:"symbols"},heartpulse:{keywords:["like","love","affection","valentines","pink"],char:'\u{1f497}',fitzpatrick_scale:!1,category:"symbols"},sparkling_heart:{keywords:["love","like","affection","valentines"],char:'\u{1f496}',fitzpatrick_scale:!1,category:"symbols"},cupid:{keywords:["love","like","heart","affection","valentines"],char:'\u{1f498}',fitzpatrick_scale:!1,category:"symbols"},gift_heart:{keywords:["love","valentines"],char:'\u{1f49d}',fitzpatrick_scale:!1,category:"symbols"},heart_decoration:{keywords:["purple-square","love","like"],char:'\u{1f49f}',fitzpatrick_scale:!1,category:"symbols"},peace_symbol:{keywords:["hippie"],char:'\u262e',fitzpatrick_scale:!1,category:"symbols"},latin_cross:{keywords:["christianity"],char:'\u271d',fitzpatrick_scale:!1,category:"symbols"},star_and_crescent:{keywords:["islam"],char:'\u262a',fitzpatrick_scale:!1,category:"symbols"},om:{keywords:["hinduism","buddhism","sikhism","jainism"],char:'\u{1f549}',fitzpatrick_scale:!1,category:"symbols"},wheel_of_dharma:{keywords:["hinduism","buddhism","sikhism","jainism"],char:'\u2638',fitzpatrick_scale:!1,category:"symbols"},star_of_david:{keywords:["judaism"],char:'\u2721',fitzpatrick_scale:!1,category:"symbols"},six_pointed_star:{keywords:["purple-square","religion","jewish","hexagram"],char:'\u{1f52f}',fitzpatrick_scale:!1,category:"symbols"},menorah:{keywords:["hanukkah","candles","jewish"],char:'\u{1f54e}',fitzpatrick_scale:!1,category:"symbols"},yin_yang:{keywords:["balance"],char:'\u262f',fitzpatrick_scale:!1,category:"symbols"},orthodox_cross:{keywords:["suppedaneum","religion"],char:'\u2626',fitzpatrick_scale:!1,category:"symbols"},place_of_worship:{keywords:["religion","church","temple","prayer"],char:'\u{1f6d0}',fitzpatrick_scale:!1,category:"symbols"},ophiuchus:{keywords:["sign","purple-square","constellation","astrology"],char:'\u26ce',fitzpatrick_scale:!1,category:"symbols"},aries:{keywords:["sign","purple-square","zodiac","astrology"],char:'\u2648',fitzpatrick_scale:!1,category:"symbols"},taurus:{keywords:["purple-square","sign","zodiac","astrology"],char:'\u2649',fitzpatrick_scale:!1,category:"symbols"},gemini:{keywords:["sign","zodiac","purple-square","astrology"],char:'\u264a',fitzpatrick_scale:!1,category:"symbols"},cancer:{keywords:["sign","zodiac","purple-square","astrology"],char:'\u264b',fitzpatrick_scale:!1,category:"symbols"},leo:{keywords:["sign","purple-square","zodiac","astrology"],char:'\u264c',fitzpatrick_scale:!1,category:"symbols"},virgo:{keywords:["sign","zodiac","purple-square","astrology"],char:'\u264d',fitzpatrick_scale:!1,category:"symbols"},libra:{keywords:["sign","purple-square","zodiac","astrology"],char:'\u264e',fitzpatrick_scale:!1,category:"symbols"},scorpius:{keywords:["sign","zodiac","purple-square","astrology","scorpio"],char:'\u264f',fitzpatrick_scale:!1,category:"symbols"},sagittarius:{keywords:["sign","zodiac","purple-square","astrology"],char:'\u2650',fitzpatrick_scale:!1,category:"symbols"},capricorn:{keywords:["sign","zodiac","purple-square","astrology"],char:'\u2651',fitzpatrick_scale:!1,category:"symbols"},aquarius:{keywords:["sign","purple-square","zodiac","astrology"],char:'\u2652',fitzpatrick_scale:!1,category:"symbols"},pisces:{keywords:["purple-square","sign","zodiac","astrology"],char:'\u2653',fitzpatrick_scale:!1,category:"symbols"},id:{keywords:["purple-square","words"],char:'\u{1f194}',fitzpatrick_scale:!1,category:"symbols"},atom_symbol:{keywords:["science","physics","chemistry"],char:'\u269b',fitzpatrick_scale:!1,category:"symbols"},u7a7a:{keywords:["kanji","japanese","chinese","empty","sky","blue-square"],char:'\u{1f233}',fitzpatrick_scale:!1,category:"symbols"},u5272:{keywords:["cut","divide","chinese","kanji","pink-square"],char:'\u{1f239}',fitzpatrick_scale:!1,category:"symbols"},radioactive:{keywords:["nuclear","danger"],char:'\u2622',fitzpatrick_scale:!1,category:"symbols"},biohazard:{keywords:["danger"],char:'\u2623',fitzpatrick_scale:!1,category:"symbols"},mobile_phone_off:{keywords:["mute","orange-square","silence","quiet"],char:'\u{1f4f4}',fitzpatrick_scale:!1,category:"symbols"},vibration_mode:{keywords:["orange-square","phone"],char:'\u{1f4f3}',fitzpatrick_scale:!1,category:"symbols"},u6709:{keywords:["orange-square","chinese","have","kanji"],char:'\u{1f236}',fitzpatrick_scale:!1,category:"symbols"},u7121:{keywords:["nothing","chinese","kanji","japanese","orange-square"],char:'\u{1f21a}',fitzpatrick_scale:!1,category:"symbols"},u7533:{keywords:["chinese","japanese","kanji","orange-square"],char:'\u{1f238}',fitzpatrick_scale:!1,category:"symbols"},u55b6:{keywords:["japanese","opening hours","orange-square"],char:'\u{1f23a}',fitzpatrick_scale:!1,category:"symbols"},u6708:{keywords:["chinese","month","moon","japanese","orange-square","kanji"],char:'\u{1f237}\ufe0f',fitzpatrick_scale:!1,category:"symbols"},eight_pointed_black_star:{keywords:["orange-square","shape","polygon"],char:'\u2734\ufe0f',fitzpatrick_scale:!1,category:"symbols"},vs:{keywords:["words","orange-square"],char:'\u{1f19a}',fitzpatrick_scale:!1,category:"symbols"},accept:{keywords:["ok","good","chinese","kanji","agree","yes","orange-circle"],char:'\u{1f251}',fitzpatrick_scale:!1,category:"symbols"},white_flower:{keywords:["japanese","spring"],char:'\u{1f4ae}',fitzpatrick_scale:!1,category:"symbols"},ideograph_advantage:{keywords:["chinese","kanji","obtain","get","circle"],char:'\u{1f250}',fitzpatrick_scale:!1,category:"symbols"},secret:{keywords:["privacy","chinese","sshh","kanji","red-circle"],char:'\u3299\ufe0f',fitzpatrick_scale:!1,category:"symbols"},congratulations:{keywords:["chinese","kanji","japanese","red-circle"],char:'\u3297\ufe0f',fitzpatrick_scale:!1,category:"symbols"},u5408:{keywords:["japanese","chinese","join","kanji","red-square"],char:'\u{1f234}',fitzpatrick_scale:!1,category:"symbols"},u6e80:{keywords:["full","chinese","japanese","red-square","kanji"],char:'\u{1f235}',fitzpatrick_scale:!1,category:"symbols"},u7981:{keywords:["kanji","japanese","chinese","forbidden","limit","restricted","red-square"],char:'\u{1f232}',fitzpatrick_scale:!1,category:"symbols"},a:{keywords:["red-square","alphabet","letter"],char:'\u{1f170}\ufe0f',fitzpatrick_scale:!1,category:"symbols"},b:{keywords:["red-square","alphabet","letter"],char:'\u{1f171}\ufe0f',fitzpatrick_scale:!1,category:"symbols"},ab:{keywords:["red-square","alphabet"],char:'\u{1f18e}',fitzpatrick_scale:!1,category:"symbols"},cl:{keywords:["alphabet","words","red-square"],char:'\u{1f191}',fitzpatrick_scale:!1,category:"symbols"},o2:{keywords:["alphabet","red-square","letter"],char:'\u{1f17e}\ufe0f',fitzpatrick_scale:!1,category:"symbols"},sos:{keywords:["help","red-square","words","emergency","911"],char:'\u{1f198}',fitzpatrick_scale:!1,category:"symbols"},no_entry:{keywords:["limit","security","privacy","bad","denied","stop","circle"],char:'\u26d4',fitzpatrick_scale:!1,category:"symbols"},name_badge:{keywords:["fire","forbid"],char:'\u{1f4db}',fitzpatrick_scale:!1,category:"symbols"},no_entry_sign:{keywords:["forbid","stop","limit","denied","disallow","circle"],char:'\u{1f6ab}',fitzpatrick_scale:!1,category:"symbols"},x:{keywords:["no","delete","remove","cancel","red"],char:'\u274c',fitzpatrick_scale:!1,category:"symbols"},o:{keywords:["circle","round"],char:'\u2b55',fitzpatrick_scale:!1,category:"symbols"},stop_sign:{keywords:["stop"],char:'\u{1f6d1}',fitzpatrick_scale:!1,category:"symbols"},anger:{keywords:["angry","mad"],char:'\u{1f4a2}',fitzpatrick_scale:!1,category:"symbols"},hotsprings:{keywords:["bath","warm","relax"],char:'\u2668\ufe0f',fitzpatrick_scale:!1,category:"symbols"},no_pedestrians:{keywords:["rules","crossing","walking","circle"],char:'\u{1f6b7}',fitzpatrick_scale:!1,category:"symbols"},do_not_litter:{keywords:["trash","bin","garbage","circle"],char:'\u{1f6af}',fitzpatrick_scale:!1,category:"symbols"},no_bicycles:{keywords:["cyclist","prohibited","circle"],char:'\u{1f6b3}',fitzpatrick_scale:!1,category:"symbols"},"non-potable_water":{keywords:["drink","faucet","tap","circle"],char:'\u{1f6b1}',fitzpatrick_scale:!1,category:"symbols"},underage:{keywords:["18","drink","pub","night","minor","circle"],char:'\u{1f51e}',fitzpatrick_scale:!1,category:"symbols"},no_mobile_phones:{keywords:["iphone","mute","circle"],char:'\u{1f4f5}',fitzpatrick_scale:!1,category:"symbols"},exclamation:{keywords:["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"],char:'\u2757',fitzpatrick_scale:!1,category:"symbols"},grey_exclamation:{keywords:["surprise","punctuation","gray","wow","warning"],char:'\u2755',fitzpatrick_scale:!1,category:"symbols"},question:{keywords:["doubt","confused"],char:'\u2753',fitzpatrick_scale:!1,category:"symbols"},grey_question:{keywords:["doubts","gray","huh","confused"],char:'\u2754',fitzpatrick_scale:!1,category:"symbols"},bangbang:{keywords:["exclamation","surprise"],char:'\u203c\ufe0f',fitzpatrick_scale:!1,category:"symbols"},interrobang:{keywords:["wat","punctuation","surprise"],char:'\u2049\ufe0f',fitzpatrick_scale:!1,category:"symbols"},low_brightness:{keywords:["sun","afternoon","warm","summer"],char:'\u{1f505}',fitzpatrick_scale:!1,category:"symbols"},high_brightness:{keywords:["sun","light"],char:'\u{1f506}',fitzpatrick_scale:!1,category:"symbols"},trident:{keywords:["weapon","spear"],char:'\u{1f531}',fitzpatrick_scale:!1,category:"symbols"},fleur_de_lis:{keywords:["decorative","scout"],char:'\u269c',fitzpatrick_scale:!1,category:"symbols"},part_alternation_mark:{keywords:["graph","presentation","stats","business","economics","bad"],char:'\u303d\ufe0f',fitzpatrick_scale:!1,category:"symbols"},warning:{keywords:["exclamation","wip","alert","error","problem","issue"],char:'\u26a0\ufe0f',fitzpatrick_scale:!1,category:"symbols"},children_crossing:{keywords:["school","warning","danger","sign","driving","yellow-diamond"],char:'\u{1f6b8}',fitzpatrick_scale:!1,category:"symbols"},beginner:{keywords:["badge","shield"],char:'\u{1f530}',fitzpatrick_scale:!1,category:"symbols"},recycle:{keywords:["arrow","environment","garbage","trash"],char:'\u267b\ufe0f',fitzpatrick_scale:!1,category:"symbols"},u6307:{keywords:["chinese","point","green-square","kanji"],char:'\u{1f22f}',fitzpatrick_scale:!1,category:"symbols"},chart:{keywords:["green-square","graph","presentation","stats"],char:'\u{1f4b9}',fitzpatrick_scale:!1,category:"symbols"},sparkle:{keywords:["stars","green-square","awesome","good","fireworks"],char:'\u2747\ufe0f',fitzpatrick_scale:!1,category:"symbols"},eight_spoked_asterisk:{keywords:["star","sparkle","green-square"],char:'\u2733\ufe0f',fitzpatrick_scale:!1,category:"symbols"},negative_squared_cross_mark:{keywords:["x","green-square","no","deny"],char:'\u274e',fitzpatrick_scale:!1,category:"symbols"},white_check_mark:{keywords:["green-square","ok","agree","vote","election","answer","tick"],char:'\u2705',fitzpatrick_scale:!1,category:"symbols"},diamond_shape_with_a_dot_inside:{keywords:["jewel","blue","gem","crystal","fancy"],char:'\u{1f4a0}',fitzpatrick_scale:!1,category:"symbols"},cyclone:{keywords:["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],char:'\u{1f300}',fitzpatrick_scale:!1,category:"symbols"},loop:{keywords:["tape","cassette"],char:'\u27bf',fitzpatrick_scale:!1,category:"symbols"},globe_with_meridians:{keywords:["earth","international","world","internet","interweb","i18n"],char:'\u{1f310}',fitzpatrick_scale:!1,category:"symbols"},m:{keywords:["alphabet","blue-circle","letter"],char:'\u24c2\ufe0f',fitzpatrick_scale:!1,category:"symbols"},atm:{keywords:["money","sales","cash","blue-square","payment","bank"],char:'\u{1f3e7}',fitzpatrick_scale:!1,category:"symbols"},sa:{keywords:["japanese","blue-square","katakana"],char:'\u{1f202}\ufe0f',fitzpatrick_scale:!1,category:"symbols"},passport_control:{keywords:["custom","blue-square"],char:'\u{1f6c2}',fitzpatrick_scale:!1,category:"symbols"},customs:{keywords:["passport","border","blue-square"],char:'\u{1f6c3}',fitzpatrick_scale:!1,category:"symbols"},baggage_claim:{keywords:["blue-square","airport","transport"],char:'\u{1f6c4}',fitzpatrick_scale:!1,category:"symbols"},left_luggage:{keywords:["blue-square","travel"],char:'\u{1f6c5}',fitzpatrick_scale:!1,category:"symbols"},wheelchair:{keywords:["blue-square","disabled","a11y","accessibility"],char:'\u267f',fitzpatrick_scale:!1,category:"symbols"},no_smoking:{keywords:["cigarette","blue-square","smell","smoke"],char:'\u{1f6ad}',fitzpatrick_scale:!1,category:"symbols"},wc:{keywords:["toilet","restroom","blue-square"],char:'\u{1f6be}',fitzpatrick_scale:!1,category:"symbols"},parking:{keywords:["cars","blue-square","alphabet","letter"],char:'\u{1f17f}\ufe0f',fitzpatrick_scale:!1,category:"symbols"},potable_water:{keywords:["blue-square","liquid","restroom","cleaning","faucet"],char:'\u{1f6b0}',fitzpatrick_scale:!1,category:"symbols"},mens:{keywords:["toilet","restroom","wc","blue-square","gender","male"],char:'\u{1f6b9}',fitzpatrick_scale:!1,category:"symbols"},womens:{keywords:["purple-square","woman","female","toilet","loo","restroom","gender"],char:'\u{1f6ba}',fitzpatrick_scale:!1,category:"symbols"},baby_symbol:{keywords:["orange-square","child"],char:'\u{1f6bc}',fitzpatrick_scale:!1,category:"symbols"},restroom:{keywords:["blue-square","toilet","refresh","wc","gender"],char:'\u{1f6bb}',fitzpatrick_scale:!1,category:"symbols"},put_litter_in_its_place:{keywords:["blue-square","sign","human","info"],char:'\u{1f6ae}',fitzpatrick_scale:!1,category:"symbols"},cinema:{keywords:["blue-square","record","film","movie","curtain","stage","theater"],char:'\u{1f3a6}',fitzpatrick_scale:!1,category:"symbols"},signal_strength:{keywords:["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"],char:'\u{1f4f6}',fitzpatrick_scale:!1,category:"symbols"},koko:{keywords:["blue-square","here","katakana","japanese","destination"],char:'\u{1f201}',fitzpatrick_scale:!1,category:"symbols"},ng:{keywords:["blue-square","words","shape","icon"],char:'\u{1f196}',fitzpatrick_scale:!1,category:"symbols"},ok:{keywords:["good","agree","yes","blue-square"],char:'\u{1f197}',fitzpatrick_scale:!1,category:"symbols"},up:{keywords:["blue-square","above","high"],char:'\u{1f199}',fitzpatrick_scale:!1,category:"symbols"},cool:{keywords:["words","blue-square"],char:'\u{1f192}',fitzpatrick_scale:!1,category:"symbols"},new:{keywords:["blue-square","words","start"],char:'\u{1f195}',fitzpatrick_scale:!1,category:"symbols"},free:{keywords:["blue-square","words"],char:'\u{1f193}',fitzpatrick_scale:!1,category:"symbols"},zero:{keywords:["0","numbers","blue-square","null"],char:'0\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},one:{keywords:["blue-square","numbers","1"],char:'1\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},two:{keywords:["numbers","2","prime","blue-square"],char:'2\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},three:{keywords:["3","numbers","prime","blue-square"],char:'3\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},four:{keywords:["4","numbers","blue-square"],char:'4\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},five:{keywords:["5","numbers","blue-square","prime"],char:'5\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},six:{keywords:["6","numbers","blue-square"],char:'6\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},seven:{keywords:["7","numbers","blue-square","prime"],char:'7\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},eight:{keywords:["8","blue-square","numbers"],char:'8\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},nine:{keywords:["blue-square","numbers","9"],char:'9\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},keycap_ten:{keywords:["numbers","10","blue-square"],char:'\u{1f51f}',fitzpatrick_scale:!1,category:"symbols"},asterisk:{keywords:["star","keycap"],char:'*\u20e3',fitzpatrick_scale:!1,category:"symbols"},eject_button:{keywords:["blue-square"],char:'\u23cf\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_forward:{keywords:["blue-square","right","direction","play"],char:'\u25b6\ufe0f',fitzpatrick_scale:!1,category:"symbols"},pause_button:{keywords:["pause","blue-square"],char:'\u23f8',fitzpatrick_scale:!1,category:"symbols"},next_track_button:{keywords:["forward","next","blue-square"],char:'\u23ed',fitzpatrick_scale:!1,category:"symbols"},stop_button:{keywords:["blue-square"],char:'\u23f9',fitzpatrick_scale:!1,category:"symbols"},record_button:{keywords:["blue-square"],char:'\u23fa',fitzpatrick_scale:!1,category:"symbols"},play_or_pause_button:{keywords:["blue-square","play","pause"],char:'\u23ef',fitzpatrick_scale:!1,category:"symbols"},previous_track_button:{keywords:["backward"],char:'\u23ee',fitzpatrick_scale:!1,category:"symbols"},fast_forward:{keywords:["blue-square","play","speed","continue"],char:'\u23e9',fitzpatrick_scale:!1,category:"symbols"},rewind:{keywords:["play","blue-square"],char:'\u23ea',fitzpatrick_scale:!1,category:"symbols"},twisted_rightwards_arrows:{keywords:["blue-square","shuffle","music","random"],char:'\u{1f500}',fitzpatrick_scale:!1,category:"symbols"},repeat:{keywords:["loop","record"],char:'\u{1f501}',fitzpatrick_scale:!1,category:"symbols"},repeat_one:{keywords:["blue-square","loop"],char:'\u{1f502}',fitzpatrick_scale:!1,category:"symbols"},arrow_backward:{keywords:["blue-square","left","direction"],char:'\u25c0\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_up_small:{keywords:["blue-square","triangle","direction","point","forward","top"],char:'\u{1f53c}',fitzpatrick_scale:!1,category:"symbols"},arrow_down_small:{keywords:["blue-square","direction","bottom"],char:'\u{1f53d}',fitzpatrick_scale:!1,category:"symbols"},arrow_double_up:{keywords:["blue-square","direction","top"],char:'\u23eb',fitzpatrick_scale:!1,category:"symbols"},arrow_double_down:{keywords:["blue-square","direction","bottom"],char:'\u23ec',fitzpatrick_scale:!1,category:"symbols"},arrow_right:{keywords:["blue-square","next"],char:'\u27a1\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_left:{keywords:["blue-square","previous","back"],char:'\u2b05\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_up:{keywords:["blue-square","continue","top","direction"],char:'\u2b06\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_down:{keywords:["blue-square","direction","bottom"],char:'\u2b07\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_upper_right:{keywords:["blue-square","point","direction","diagonal","northeast"],char:'\u2197\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_lower_right:{keywords:["blue-square","direction","diagonal","southeast"],char:'\u2198\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_lower_left:{keywords:["blue-square","direction","diagonal","southwest"],char:'\u2199\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_upper_left:{keywords:["blue-square","point","direction","diagonal","northwest"],char:'\u2196\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_up_down:{keywords:["blue-square","direction","way","vertical"],char:'\u2195\ufe0f',fitzpatrick_scale:!1,category:"symbols"},left_right_arrow:{keywords:["shape","direction","horizontal","sideways"],char:'\u2194\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrows_counterclockwise:{keywords:["blue-square","sync","cycle"],char:'\u{1f504}',fitzpatrick_scale:!1,category:"symbols"},arrow_right_hook:{keywords:["blue-square","return","rotate","direction"],char:'\u21aa\ufe0f',fitzpatrick_scale:!1,category:"symbols"},leftwards_arrow_with_hook:{keywords:["back","return","blue-square","undo","enter"],char:'\u21a9\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_heading_up:{keywords:["blue-square","direction","top"],char:'\u2934\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrow_heading_down:{keywords:["blue-square","direction","bottom"],char:'\u2935\ufe0f',fitzpatrick_scale:!1,category:"symbols"},hash:{keywords:["symbol","blue-square","twitter"],char:'#\ufe0f\u20e3',fitzpatrick_scale:!1,category:"symbols"},information_source:{keywords:["blue-square","alphabet","letter"],char:'\u2139\ufe0f',fitzpatrick_scale:!1,category:"symbols"},abc:{keywords:["blue-square","alphabet"],char:'\u{1f524}',fitzpatrick_scale:!1,category:"symbols"},abcd:{keywords:["blue-square","alphabet"],char:'\u{1f521}',fitzpatrick_scale:!1,category:"symbols"},capital_abcd:{keywords:["alphabet","words","blue-square"],char:'\u{1f520}',fitzpatrick_scale:!1,category:"symbols"},symbols:{keywords:["blue-square","music","note","ampersand","percent","glyphs","characters"],char:'\u{1f523}',fitzpatrick_scale:!1,category:"symbols"},musical_note:{keywords:["score","tone","sound"],char:'\u{1f3b5}',fitzpatrick_scale:!1,category:"symbols"},notes:{keywords:["music","score"],char:'\u{1f3b6}',fitzpatrick_scale:!1,category:"symbols"},wavy_dash:{keywords:["draw","line","moustache","mustache","squiggle","scribble"],char:'\u3030\ufe0f',fitzpatrick_scale:!1,category:"symbols"},curly_loop:{keywords:["scribble","draw","shape","squiggle"],char:'\u27b0',fitzpatrick_scale:!1,category:"symbols"},heavy_check_mark:{keywords:["ok","nike","answer","yes","tick"],char:'\u2714\ufe0f',fitzpatrick_scale:!1,category:"symbols"},arrows_clockwise:{keywords:["sync","cycle","round","repeat"],char:'\u{1f503}',fitzpatrick_scale:!1,category:"symbols"},heavy_plus_sign:{keywords:["math","calculation","addition","more","increase"],char:'\u2795',fitzpatrick_scale:!1,category:"symbols"},heavy_minus_sign:{keywords:["math","calculation","subtract","less"],char:'\u2796',fitzpatrick_scale:!1,category:"symbols"},heavy_division_sign:{keywords:["divide","math","calculation"],char:'\u2797',fitzpatrick_scale:!1,category:"symbols"},heavy_multiplication_x:{keywords:["math","calculation"],char:'\u2716\ufe0f',fitzpatrick_scale:!1,category:"symbols"},infinity:{keywords:["forever"],char:'\u267e',fitzpatrick_scale:!1,category:"symbols"},heavy_dollar_sign:{keywords:["money","sales","payment","currency","buck"],char:'\u{1f4b2}',fitzpatrick_scale:!1,category:"symbols"},currency_exchange:{keywords:["money","sales","dollar","travel"],char:'\u{1f4b1}',fitzpatrick_scale:!1,category:"symbols"},copyright:{keywords:["ip","license","circle","law","legal"],char:'\xa9\ufe0f',fitzpatrick_scale:!1,category:"symbols"},registered:{keywords:["alphabet","circle"],char:'\xae\ufe0f',fitzpatrick_scale:!1,category:"symbols"},tm:{keywords:["trademark","brand","law","legal"],char:'\u2122\ufe0f',fitzpatrick_scale:!1,category:"symbols"},end:{keywords:["words","arrow"],char:'\u{1f51a}',fitzpatrick_scale:!1,category:"symbols"},back:{keywords:["arrow","words","return"],char:'\u{1f519}',fitzpatrick_scale:!1,category:"symbols"},on:{keywords:["arrow","words"],char:'\u{1f51b}',fitzpatrick_scale:!1,category:"symbols"},top:{keywords:["words","blue-square"],char:'\u{1f51d}',fitzpatrick_scale:!1,category:"symbols"},soon:{keywords:["arrow","words"],char:'\u{1f51c}',fitzpatrick_scale:!1,category:"symbols"},ballot_box_with_check:{keywords:["ok","agree","confirm","black-square","vote","election","yes","tick"],char:'\u2611\ufe0f',fitzpatrick_scale:!1,category:"symbols"},radio_button:{keywords:["input","old","music","circle"],char:'\u{1f518}',fitzpatrick_scale:!1,category:"symbols"},white_circle:{keywords:["shape","round"],char:'\u26aa',fitzpatrick_scale:!1,category:"symbols"},black_circle:{keywords:["shape","button","round"],char:'\u26ab',fitzpatrick_scale:!1,category:"symbols"},red_circle:{keywords:["shape","error","danger"],char:'\u{1f534}',fitzpatrick_scale:!1,category:"symbols"},large_blue_circle:{keywords:["shape","icon","button"],char:'\u{1f535}',fitzpatrick_scale:!1,category:"symbols"},small_orange_diamond:{keywords:["shape","jewel","gem"],char:'\u{1f538}',fitzpatrick_scale:!1,category:"symbols"},small_blue_diamond:{keywords:["shape","jewel","gem"],char:'\u{1f539}',fitzpatrick_scale:!1,category:"symbols"},large_orange_diamond:{keywords:["shape","jewel","gem"],char:'\u{1f536}',fitzpatrick_scale:!1,category:"symbols"},large_blue_diamond:{keywords:["shape","jewel","gem"],char:'\u{1f537}',fitzpatrick_scale:!1,category:"symbols"},small_red_triangle:{keywords:["shape","direction","up","top"],char:'\u{1f53a}',fitzpatrick_scale:!1,category:"symbols"},black_small_square:{keywords:["shape","icon"],char:'\u25aa\ufe0f',fitzpatrick_scale:!1,category:"symbols"},white_small_square:{keywords:["shape","icon"],char:'\u25ab\ufe0f',fitzpatrick_scale:!1,category:"symbols"},black_large_square:{keywords:["shape","icon","button"],char:'\u2b1b',fitzpatrick_scale:!1,category:"symbols"},white_large_square:{keywords:["shape","icon","stone","button"],char:'\u2b1c',fitzpatrick_scale:!1,category:"symbols"},small_red_triangle_down:{keywords:["shape","direction","bottom"],char:'\u{1f53b}',fitzpatrick_scale:!1,category:"symbols"},black_medium_square:{keywords:["shape","button","icon"],char:'\u25fc\ufe0f',fitzpatrick_scale:!1,category:"symbols"},white_medium_square:{keywords:["shape","stone","icon"],char:'\u25fb\ufe0f',fitzpatrick_scale:!1,category:"symbols"},black_medium_small_square:{keywords:["icon","shape","button"],char:'\u25fe',fitzpatrick_scale:!1,category:"symbols"},white_medium_small_square:{keywords:["shape","stone","icon","button"],char:'\u25fd',fitzpatrick_scale:!1,category:"symbols"},black_square_button:{keywords:["shape","input","frame"],char:'\u{1f532}',fitzpatrick_scale:!1,category:"symbols"},white_square_button:{keywords:["shape","input"],char:'\u{1f533}',fitzpatrick_scale:!1,category:"symbols"},speaker:{keywords:["sound","volume","silence","broadcast"],char:'\u{1f508}',fitzpatrick_scale:!1,category:"symbols"},sound:{keywords:["volume","speaker","broadcast"],char:'\u{1f509}',fitzpatrick_scale:!1,category:"symbols"},loud_sound:{keywords:["volume","noise","noisy","speaker","broadcast"],char:'\u{1f50a}',fitzpatrick_scale:!1,category:"symbols"},mute:{keywords:["sound","volume","silence","quiet"],char:'\u{1f507}',fitzpatrick_scale:!1,category:"symbols"},mega:{keywords:["sound","speaker","volume"],char:'\u{1f4e3}',fitzpatrick_scale:!1,category:"symbols"},loudspeaker:{keywords:["volume","sound"],char:'\u{1f4e2}',fitzpatrick_scale:!1,category:"symbols"},bell:{keywords:["sound","notification","christmas","xmas","chime"],char:'\u{1f514}',fitzpatrick_scale:!1,category:"symbols"},no_bell:{keywords:["sound","volume","mute","quiet","silent"],char:'\u{1f515}',fitzpatrick_scale:!1,category:"symbols"},black_joker:{keywords:["poker","cards","game","play","magic"],char:'\u{1f0cf}',fitzpatrick_scale:!1,category:"symbols"},mahjong:{keywords:["game","play","chinese","kanji"],char:'\u{1f004}',fitzpatrick_scale:!1,category:"symbols"},spades:{keywords:["poker","cards","suits","magic"],char:'\u2660\ufe0f',fitzpatrick_scale:!1,category:"symbols"},clubs:{keywords:["poker","cards","magic","suits"],char:'\u2663\ufe0f',fitzpatrick_scale:!1,category:"symbols"},hearts:{keywords:["poker","cards","magic","suits"],char:'\u2665\ufe0f',fitzpatrick_scale:!1,category:"symbols"},diamonds:{keywords:["poker","cards","magic","suits"],char:'\u2666\ufe0f',fitzpatrick_scale:!1,category:"symbols"},flower_playing_cards:{keywords:["game","sunset","red"],char:'\u{1f3b4}',fitzpatrick_scale:!1,category:"symbols"},thought_balloon:{keywords:["bubble","cloud","speech","thinking","dream"],char:'\u{1f4ad}',fitzpatrick_scale:!1,category:"symbols"},right_anger_bubble:{keywords:["caption","speech","thinking","mad"],char:'\u{1f5ef}',fitzpatrick_scale:!1,category:"symbols"},speech_balloon:{keywords:["bubble","words","message","talk","chatting"],char:'\u{1f4ac}',fitzpatrick_scale:!1,category:"symbols"},left_speech_bubble:{keywords:["words","message","talk","chatting"],char:'\u{1f5e8}',fitzpatrick_scale:!1,category:"symbols"},clock1:{keywords:["time","late","early","schedule"],char:'\u{1f550}',fitzpatrick_scale:!1,category:"symbols"},clock2:{keywords:["time","late","early","schedule"],char:'\u{1f551}',fitzpatrick_scale:!1,category:"symbols"},clock3:{keywords:["time","late","early","schedule"],char:'\u{1f552}',fitzpatrick_scale:!1,category:"symbols"},clock4:{keywords:["time","late","early","schedule"],char:'\u{1f553}',fitzpatrick_scale:!1,category:"symbols"},clock5:{keywords:["time","late","early","schedule"],char:'\u{1f554}',fitzpatrick_scale:!1,category:"symbols"},clock6:{keywords:["time","late","early","schedule","dawn","dusk"],char:'\u{1f555}',fitzpatrick_scale:!1,category:"symbols"},clock7:{keywords:["time","late","early","schedule"],char:'\u{1f556}',fitzpatrick_scale:!1,category:"symbols"},clock8:{keywords:["time","late","early","schedule"],char:'\u{1f557}',fitzpatrick_scale:!1,category:"symbols"},clock9:{keywords:["time","late","early","schedule"],char:'\u{1f558}',fitzpatrick_scale:!1,category:"symbols"},clock10:{keywords:["time","late","early","schedule"],char:'\u{1f559}',fitzpatrick_scale:!1,category:"symbols"},clock11:{keywords:["time","late","early","schedule"],char:'\u{1f55a}',fitzpatrick_scale:!1,category:"symbols"},clock12:{keywords:["time","noon","midnight","midday","late","early","schedule"],char:'\u{1f55b}',fitzpatrick_scale:!1,category:"symbols"},clock130:{keywords:["time","late","early","schedule"],char:'\u{1f55c}',fitzpatrick_scale:!1,category:"symbols"},clock230:{keywords:["time","late","early","schedule"],char:'\u{1f55d}',fitzpatrick_scale:!1,category:"symbols"},clock330:{keywords:["time","late","early","schedule"],char:'\u{1f55e}',fitzpatrick_scale:!1,category:"symbols"},clock430:{keywords:["time","late","early","schedule"],char:'\u{1f55f}',fitzpatrick_scale:!1,category:"symbols"},clock530:{keywords:["time","late","early","schedule"],char:'\u{1f560}',fitzpatrick_scale:!1,category:"symbols"},clock630:{keywords:["time","late","early","schedule"],char:'\u{1f561}',fitzpatrick_scale:!1,category:"symbols"},clock730:{keywords:["time","late","early","schedule"],char:'\u{1f562}',fitzpatrick_scale:!1,category:"symbols"},clock830:{keywords:["time","late","early","schedule"],char:'\u{1f563}',fitzpatrick_scale:!1,category:"symbols"},clock930:{keywords:["time","late","early","schedule"],char:'\u{1f564}',fitzpatrick_scale:!1,category:"symbols"},clock1030:{keywords:["time","late","early","schedule"],char:'\u{1f565}',fitzpatrick_scale:!1,category:"symbols"},clock1130:{keywords:["time","late","early","schedule"],char:'\u{1f566}',fitzpatrick_scale:!1,category:"symbols"},clock1230:{keywords:["time","late","early","schedule"],char:'\u{1f567}',fitzpatrick_scale:!1,category:"symbols"},afghanistan:{keywords:["af","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},aland_islands:{keywords:["\xc5land","islands","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1fd}',fitzpatrick_scale:!1,category:"flags"},albania:{keywords:["al","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},algeria:{keywords:["dz","flag","nation","country","banner"],char:'\u{1f1e9}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},american_samoa:{keywords:["american","ws","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},andorra:{keywords:["ad","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},angola:{keywords:["ao","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},anguilla:{keywords:["ai","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},antarctica:{keywords:["aq","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1f6}',fitzpatrick_scale:!1,category:"flags"},antigua_barbuda:{keywords:["antigua","barbuda","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},argentina:{keywords:["ar","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},armenia:{keywords:["am","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},aruba:{keywords:["aw","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},australia:{keywords:["au","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},austria:{keywords:["at","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},azerbaijan:{keywords:["az","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},bahamas:{keywords:["bs","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},bahrain:{keywords:["bh","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},bangladesh:{keywords:["bd","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},barbados:{keywords:["bb","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1e7}',fitzpatrick_scale:!1,category:"flags"},belarus:{keywords:["by","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},belgium:{keywords:["be","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},belize:{keywords:["bz","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},benin:{keywords:["bj","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1ef}',fitzpatrick_scale:!1,category:"flags"},bermuda:{keywords:["bm","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},bhutan:{keywords:["bt","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},bolivia:{keywords:["bo","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},caribbean_netherlands:{keywords:["bonaire","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f6}',fitzpatrick_scale:!1,category:"flags"},bosnia_herzegovina:{keywords:["bosnia","herzegovina","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},botswana:{keywords:["bw","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},brazil:{keywords:["br","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},british_indian_ocean_territory:{keywords:["british","indian","ocean","territory","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},british_virgin_islands:{keywords:["british","virgin","islands","bvi","flag","nation","country","banner"],char:'\u{1f1fb}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},brunei:{keywords:["bn","darussalam","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},bulgaria:{keywords:["bg","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},burkina_faso:{keywords:["burkina","faso","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},burundi:{keywords:["bi","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},cape_verde:{keywords:["cabo","verde","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1fb}',fitzpatrick_scale:!1,category:"flags"},cambodia:{keywords:["kh","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},cameroon:{keywords:["cm","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},canada:{keywords:["ca","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},canary_islands:{keywords:["canary","islands","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},cayman_islands:{keywords:["cayman","islands","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},central_african_republic:{keywords:["central","african","republic","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},chad:{keywords:["td","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},chile:{keywords:["flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},cn:{keywords:["china","chinese","prc","flag","country","nation","banner"],char:'\u{1f1e8}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},christmas_island:{keywords:["christmas","island","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1fd}',fitzpatrick_scale:!1,category:"flags"},cocos_islands:{keywords:["cocos","keeling","islands","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},colombia:{keywords:["co","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},comoros:{keywords:["km","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},congo_brazzaville:{keywords:["congo","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},congo_kinshasa:{keywords:["congo","democratic","republic","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},cook_islands:{keywords:["cook","islands","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},costa_rica:{keywords:["costa","rica","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},croatia:{keywords:["hr","flag","nation","country","banner"],char:'\u{1f1ed}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},cuba:{keywords:["cu","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},curacao:{keywords:["cura\xe7ao","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},cyprus:{keywords:["cy","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},czech_republic:{keywords:["cz","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},denmark:{keywords:["dk","flag","nation","country","banner"],char:'\u{1f1e9}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},djibouti:{keywords:["dj","flag","nation","country","banner"],char:'\u{1f1e9}\u{1f1ef}',fitzpatrick_scale:!1,category:"flags"},dominica:{keywords:["dm","flag","nation","country","banner"],char:'\u{1f1e9}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},dominican_republic:{keywords:["dominican","republic","flag","nation","country","banner"],char:'\u{1f1e9}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},ecuador:{keywords:["ec","flag","nation","country","banner"],char:'\u{1f1ea}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},egypt:{keywords:["eg","flag","nation","country","banner"],char:'\u{1f1ea}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},el_salvador:{keywords:["el","salvador","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1fb}',fitzpatrick_scale:!1,category:"flags"},equatorial_guinea:{keywords:["equatorial","gn","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f6}',fitzpatrick_scale:!1,category:"flags"},eritrea:{keywords:["er","flag","nation","country","banner"],char:'\u{1f1ea}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},estonia:{keywords:["ee","flag","nation","country","banner"],char:'\u{1f1ea}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},ethiopia:{keywords:["et","flag","nation","country","banner"],char:'\u{1f1ea}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},eu:{keywords:["european","union","flag","banner"],char:'\u{1f1ea}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},falkland_islands:{keywords:["falkland","islands","malvinas","flag","nation","country","banner"],char:'\u{1f1eb}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},faroe_islands:{keywords:["faroe","islands","flag","nation","country","banner"],char:'\u{1f1eb}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},fiji:{keywords:["fj","flag","nation","country","banner"],char:'\u{1f1eb}\u{1f1ef}',fitzpatrick_scale:!1,category:"flags"},finland:{keywords:["fi","flag","nation","country","banner"],char:'\u{1f1eb}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},fr:{keywords:["banner","flag","nation","france","french","country"],char:'\u{1f1eb}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},french_guiana:{keywords:["french","guiana","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},french_polynesia:{keywords:["french","polynesia","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},french_southern_territories:{keywords:["french","southern","territories","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},gabon:{keywords:["ga","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},gambia:{keywords:["gm","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},georgia:{keywords:["ge","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},de:{keywords:["german","nation","flag","country","banner"],char:'\u{1f1e9}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},ghana:{keywords:["gh","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},gibraltar:{keywords:["gi","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},greece:{keywords:["gr","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},greenland:{keywords:["gl","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},grenada:{keywords:["gd","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},guadeloupe:{keywords:["gp","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f5}',fitzpatrick_scale:!1,category:"flags"},guam:{keywords:["gu","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},guatemala:{keywords:["gt","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},guernsey:{keywords:["gg","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},guinea:{keywords:["gn","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},guinea_bissau:{keywords:["gw","bissau","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},guyana:{keywords:["gy","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},haiti:{keywords:["ht","flag","nation","country","banner"],char:'\u{1f1ed}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},honduras:{keywords:["hn","flag","nation","country","banner"],char:'\u{1f1ed}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},hong_kong:{keywords:["hong","kong","flag","nation","country","banner"],char:'\u{1f1ed}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},hungary:{keywords:["hu","flag","nation","country","banner"],char:'\u{1f1ed}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},iceland:{keywords:["is","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},india:{keywords:["in","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},indonesia:{keywords:["flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},iran:{keywords:["iran,","islamic","republic","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},iraq:{keywords:["iq","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f6}',fitzpatrick_scale:!1,category:"flags"},ireland:{keywords:["ie","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},isle_of_man:{keywords:["isle","man","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},israel:{keywords:["il","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},it:{keywords:["italy","flag","nation","country","banner"],char:'\u{1f1ee}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},cote_divoire:{keywords:["ivory","coast","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},jamaica:{keywords:["jm","flag","nation","country","banner"],char:'\u{1f1ef}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},jp:{keywords:["japanese","nation","flag","country","banner"],char:'\u{1f1ef}\u{1f1f5}',fitzpatrick_scale:!1,category:"flags"},jersey:{keywords:["je","flag","nation","country","banner"],char:'\u{1f1ef}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},jordan:{keywords:["jo","flag","nation","country","banner"],char:'\u{1f1ef}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},kazakhstan:{keywords:["kz","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},kenya:{keywords:["ke","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},kiribati:{keywords:["ki","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},kosovo:{keywords:["xk","flag","nation","country","banner"],char:'\u{1f1fd}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},kuwait:{keywords:["kw","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},kyrgyzstan:{keywords:["kg","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},laos:{keywords:["lao","democratic","republic","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},latvia:{keywords:["lv","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1fb}',fitzpatrick_scale:!1,category:"flags"},lebanon:{keywords:["lb","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1e7}',fitzpatrick_scale:!1,category:"flags"},lesotho:{keywords:["ls","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},liberia:{keywords:["lr","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},libya:{keywords:["ly","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},liechtenstein:{keywords:["li","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},lithuania:{keywords:["lt","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},luxembourg:{keywords:["lu","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},macau:{keywords:["macao","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},macedonia:{keywords:["macedonia,","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},madagascar:{keywords:["mg","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},malawi:{keywords:["mw","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},malaysia:{keywords:["my","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},maldives:{keywords:["mv","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1fb}',fitzpatrick_scale:!1,category:"flags"},mali:{keywords:["ml","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},malta:{keywords:["mt","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},marshall_islands:{keywords:["marshall","islands","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},martinique:{keywords:["mq","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f6}',fitzpatrick_scale:!1,category:"flags"},mauritania:{keywords:["mr","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},mauritius:{keywords:["mu","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},mayotte:{keywords:["yt","flag","nation","country","banner"],char:'\u{1f1fe}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},mexico:{keywords:["mx","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1fd}',fitzpatrick_scale:!1,category:"flags"},micronesia:{keywords:["micronesia,","federated","states","flag","nation","country","banner"],char:'\u{1f1eb}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},moldova:{keywords:["moldova,","republic","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},monaco:{keywords:["mc","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},mongolia:{keywords:["mn","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},montenegro:{keywords:["me","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},montserrat:{keywords:["ms","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},morocco:{keywords:["ma","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},mozambique:{keywords:["mz","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},myanmar:{keywords:["mm","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},namibia:{keywords:["na","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},nauru:{keywords:["nr","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},nepal:{keywords:["np","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1f5}',fitzpatrick_scale:!1,category:"flags"},netherlands:{keywords:["nl","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},new_caledonia:{keywords:["new","caledonia","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},new_zealand:{keywords:["new","zealand","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},nicaragua:{keywords:["ni","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},niger:{keywords:["ne","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},nigeria:{keywords:["flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},niue:{keywords:["nu","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},norfolk_island:{keywords:["norfolk","island","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},northern_mariana_islands:{keywords:["northern","mariana","islands","flag","nation","country","banner"],char:'\u{1f1f2}\u{1f1f5}',fitzpatrick_scale:!1,category:"flags"},north_korea:{keywords:["north","korea","nation","flag","country","banner"],char:'\u{1f1f0}\u{1f1f5}',fitzpatrick_scale:!1,category:"flags"},norway:{keywords:["no","flag","nation","country","banner"],char:'\u{1f1f3}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},oman:{keywords:["om_symbol","flag","nation","country","banner"],char:'\u{1f1f4}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},pakistan:{keywords:["pk","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},palau:{keywords:["pw","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},palestinian_territories:{keywords:["palestine","palestinian","territories","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},panama:{keywords:["pa","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},papua_new_guinea:{keywords:["papua","new","guinea","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},paraguay:{keywords:["py","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},peru:{keywords:["pe","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},philippines:{keywords:["ph","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},pitcairn_islands:{keywords:["pitcairn","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},poland:{keywords:["pl","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},portugal:{keywords:["pt","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},puerto_rico:{keywords:["puerto","rico","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},qatar:{keywords:["qa","flag","nation","country","banner"],char:'\u{1f1f6}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},reunion:{keywords:["r\xe9union","flag","nation","country","banner"],char:'\u{1f1f7}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},romania:{keywords:["ro","flag","nation","country","banner"],char:'\u{1f1f7}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},ru:{keywords:["russian","federation","flag","nation","country","banner"],char:'\u{1f1f7}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},rwanda:{keywords:["rw","flag","nation","country","banner"],char:'\u{1f1f7}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},st_barthelemy:{keywords:["saint","barth\xe9lemy","flag","nation","country","banner"],char:'\u{1f1e7}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},st_helena:{keywords:["saint","helena","ascension","tristan","cunha","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},st_kitts_nevis:{keywords:["saint","kitts","nevis","flag","nation","country","banner"],char:'\u{1f1f0}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},st_lucia:{keywords:["saint","lucia","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},st_pierre_miquelon:{keywords:["saint","pierre","miquelon","flag","nation","country","banner"],char:'\u{1f1f5}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},st_vincent_grenadines:{keywords:["saint","vincent","grenadines","flag","nation","country","banner"],char:'\u{1f1fb}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},samoa:{keywords:["ws","flag","nation","country","banner"],char:'\u{1f1fc}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},san_marino:{keywords:["san","marino","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},sao_tome_principe:{keywords:["sao","tome","principe","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},saudi_arabia:{keywords:["flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},senegal:{keywords:["sn","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},serbia:{keywords:["rs","flag","nation","country","banner"],char:'\u{1f1f7}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},seychelles:{keywords:["sc","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},sierra_leone:{keywords:["sierra","leone","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},singapore:{keywords:["sg","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},sint_maarten:{keywords:["sint","maarten","dutch","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1fd}',fitzpatrick_scale:!1,category:"flags"},slovakia:{keywords:["sk","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},slovenia:{keywords:["si","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},solomon_islands:{keywords:["solomon","islands","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1e7}',fitzpatrick_scale:!1,category:"flags"},somalia:{keywords:["so","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},south_africa:{keywords:["south","africa","flag","nation","country","banner"],char:'\u{1f1ff}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},south_georgia_south_sandwich_islands:{keywords:["south","georgia","sandwich","islands","flag","nation","country","banner"],char:'\u{1f1ec}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},kr:{keywords:["south","korea","nation","flag","country","banner"],char:'\u{1f1f0}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},south_sudan:{keywords:["south","sd","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},es:{keywords:["spain","flag","nation","country","banner"],char:'\u{1f1ea}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},sri_lanka:{keywords:["sri","lanka","flag","nation","country","banner"],char:'\u{1f1f1}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},sudan:{keywords:["sd","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1e9}',fitzpatrick_scale:!1,category:"flags"},suriname:{keywords:["sr","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},swaziland:{keywords:["sz","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},sweden:{keywords:["se","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},switzerland:{keywords:["ch","flag","nation","country","banner"],char:'\u{1f1e8}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},syria:{keywords:["syrian","arab","republic","flag","nation","country","banner"],char:'\u{1f1f8}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},taiwan:{keywords:["tw","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},tajikistan:{keywords:["tj","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1ef}',fitzpatrick_scale:!1,category:"flags"},tanzania:{keywords:["tanzania,","united","republic","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},thailand:{keywords:["th","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},timor_leste:{keywords:["timor","leste","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1f1}',fitzpatrick_scale:!1,category:"flags"},togo:{keywords:["tg","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},tokelau:{keywords:["tk","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1f0}',fitzpatrick_scale:!1,category:"flags"},tonga:{keywords:["to","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1f4}',fitzpatrick_scale:!1,category:"flags"},trinidad_tobago:{keywords:["trinidad","tobago","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1f9}',fitzpatrick_scale:!1,category:"flags"},tunisia:{keywords:["tn","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},tr:{keywords:["turkey","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1f7}',fitzpatrick_scale:!1,category:"flags"},turkmenistan:{keywords:["flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},turks_caicos_islands:{keywords:["turks","caicos","islands","flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1e8}',fitzpatrick_scale:!1,category:"flags"},tuvalu:{keywords:["flag","nation","country","banner"],char:'\u{1f1f9}\u{1f1fb}',fitzpatrick_scale:!1,category:"flags"},uganda:{keywords:["ug","flag","nation","country","banner"],char:'\u{1f1fa}\u{1f1ec}',fitzpatrick_scale:!1,category:"flags"},ukraine:{keywords:["ua","flag","nation","country","banner"],char:'\u{1f1fa}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},united_arab_emirates:{keywords:["united","arab","emirates","flag","nation","country","banner"],char:'\u{1f1e6}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},uk:{keywords:["united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","UK","english","england","union jack"],char:'\u{1f1ec}\u{1f1e7}',fitzpatrick_scale:!1,category:"flags"},england:{keywords:["flag","english"],char:'\u{1f3f4}\u{e0067}\u{e0062}\u{e0065}\u{e006e}\u{e0067}\u{e007f}',fitzpatrick_scale:!1,category:"flags"},scotland:{keywords:["flag","scottish"],char:'\u{1f3f4}\u{e0067}\u{e0062}\u{e0073}\u{e0063}\u{e0074}\u{e007f}',fitzpatrick_scale:!1,category:"flags"},wales:{keywords:["flag","welsh"],char:'\u{1f3f4}\u{e0067}\u{e0062}\u{e0077}\u{e006c}\u{e0073}\u{e007f}',fitzpatrick_scale:!1,category:"flags"},us:{keywords:["united","states","america","flag","nation","country","banner"],char:'\u{1f1fa}\u{1f1f8}',fitzpatrick_scale:!1,category:"flags"},us_virgin_islands:{keywords:["virgin","islands","us","flag","nation","country","banner"],char:'\u{1f1fb}\u{1f1ee}',fitzpatrick_scale:!1,category:"flags"},uruguay:{keywords:["uy","flag","nation","country","banner"],char:'\u{1f1fa}\u{1f1fe}',fitzpatrick_scale:!1,category:"flags"},uzbekistan:{keywords:["uz","flag","nation","country","banner"],char:'\u{1f1fa}\u{1f1ff}',fitzpatrick_scale:!1,category:"flags"},vanuatu:{keywords:["vu","flag","nation","country","banner"],char:'\u{1f1fb}\u{1f1fa}',fitzpatrick_scale:!1,category:"flags"},vatican_city:{keywords:["vatican","city","flag","nation","country","banner"],char:'\u{1f1fb}\u{1f1e6}',fitzpatrick_scale:!1,category:"flags"},venezuela:{keywords:["ve","bolivarian","republic","flag","nation","country","banner"],char:'\u{1f1fb}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},vietnam:{keywords:["viet","nam","flag","nation","country","banner"],char:'\u{1f1fb}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},wallis_futuna:{keywords:["wallis","futuna","flag","nation","country","banner"],char:'\u{1f1fc}\u{1f1eb}',fitzpatrick_scale:!1,category:"flags"},western_sahara:{keywords:["western","sahara","flag","nation","country","banner"],char:'\u{1f1ea}\u{1f1ed}',fitzpatrick_scale:!1,category:"flags"},yemen:{keywords:["ye","flag","nation","country","banner"],char:'\u{1f1fe}\u{1f1ea}',fitzpatrick_scale:!1,category:"flags"},zambia:{keywords:["zm","flag","nation","country","banner"],char:'\u{1f1ff}\u{1f1f2}',fitzpatrick_scale:!1,category:"flags"},zimbabwe:{keywords:["zw","flag","nation","country","banner"],char:'\u{1f1ff}\u{1f1fc}',fitzpatrick_scale:!1,category:"flags"},united_nations:{keywords:["un","flag","banner"],char:'\u{1f1fa}\u{1f1f3}',fitzpatrick_scale:!1,category:"flags"},pirate_flag:{keywords:["skull","crossbones","flag","banner"],char:'\u{1f3f4}\u200d\u2620\ufe0f',fitzpatrick_scale:!1,category:"flags"}}); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/emoticons/js/emojis.js b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojis.js new file mode 100644 index 0000000..88455e9 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojis.js @@ -0,0 +1 @@ +window.tinymce.Resource.add("tinymce.plugins.emoticons",{grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:"😀",fitzpatrick_scale:false,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:"😬",fitzpatrick_scale:false,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:"😁",fitzpatrick_scale:false,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:"😂",fitzpatrick_scale:false,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:"🤣",fitzpatrick_scale:false,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:"🥳",fitzpatrick_scale:false,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:"😃",fitzpatrick_scale:false,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:"😄",fitzpatrick_scale:false,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:"😅",fitzpatrick_scale:false,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:"😆",fitzpatrick_scale:false,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:"😇",fitzpatrick_scale:false,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:"😉",fitzpatrick_scale:false,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:"😊",fitzpatrick_scale:false,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:"🙂",fitzpatrick_scale:false,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:"🙃",fitzpatrick_scale:false,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:"☺️",fitzpatrick_scale:false,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:"😋",fitzpatrick_scale:false,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:"😌",fitzpatrick_scale:false,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:"😍",fitzpatrick_scale:false,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:"🥰",fitzpatrick_scale:false,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"😘",fitzpatrick_scale:false,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:"😗",fitzpatrick_scale:false,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:"😙",fitzpatrick_scale:false,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"😚",fitzpatrick_scale:false,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:"😜",fitzpatrick_scale:false,category:"people"},zany:{keywords:["face","goofy","crazy"],char:"🤪",fitzpatrick_scale:false,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:"🤨",fitzpatrick_scale:false,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:"🧐",fitzpatrick_scale:false,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:"😝",fitzpatrick_scale:false,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:"😛",fitzpatrick_scale:false,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:"🤑",fitzpatrick_scale:false,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:"🤓",fitzpatrick_scale:false,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:"😎",fitzpatrick_scale:false,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:"🤩",fitzpatrick_scale:false,category:"people"},clown_face:{keywords:["face"],char:"🤡",fitzpatrick_scale:false,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:"🤠",fitzpatrick_scale:false,category:"people"},hugs:{keywords:["face","smile","hug"],char:"🤗",fitzpatrick_scale:false,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:"😏",fitzpatrick_scale:false,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:"😶",fitzpatrick_scale:false,category:"people"},neutral_face:{keywords:["indifference","meh",":|","neutral"],char:"😐",fitzpatrick_scale:false,category:"people"},expressionless:{keywords:["face","indifferent","-_-","meh","deadpan"],char:"😑",fitzpatrick_scale:false,category:"people"},unamused:{keywords:["indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"],char:"😒",fitzpatrick_scale:false,category:"people"},roll_eyes:{keywords:["face","eyeroll","frustrated"],char:"🙄",fitzpatrick_scale:false,category:"people"},thinking:{keywords:["face","hmmm","think","consider"],char:"🤔",fitzpatrick_scale:false,category:"people"},lying_face:{keywords:["face","lie","pinocchio"],char:"🤥",fitzpatrick_scale:false,category:"people"},hand_over_mouth:{keywords:["face","whoops","shock","surprise"],char:"🤭",fitzpatrick_scale:false,category:"people"},shushing:{keywords:["face","quiet","shhh"],char:"🤫",fitzpatrick_scale:false,category:"people"},symbols_over_mouth:{keywords:["face","swearing","cursing","cussing","profanity","expletive"],char:"🤬",fitzpatrick_scale:false,category:"people"},exploding_head:{keywords:["face","shocked","mind","blown"],char:"🤯",fitzpatrick_scale:false,category:"people"},flushed:{keywords:["face","blush","shy","flattered"],char:"😳",fitzpatrick_scale:false,category:"people"},disappointed:{keywords:["face","sad","upset","depressed",":("],char:"😞",fitzpatrick_scale:false,category:"people"},worried:{keywords:["face","concern","nervous",":("],char:"😟",fitzpatrick_scale:false,category:"people"},angry:{keywords:["mad","face","annoyed","frustrated"],char:"😠",fitzpatrick_scale:false,category:"people"},rage:{keywords:["angry","mad","hate","despise"],char:"😡",fitzpatrick_scale:false,category:"people"},pensive:{keywords:["face","sad","depressed","upset"],char:"😔",fitzpatrick_scale:false,category:"people"},confused:{keywords:["face","indifference","huh","weird","hmmm",":/"],char:"😕",fitzpatrick_scale:false,category:"people"},slightly_frowning_face:{keywords:["face","frowning","disappointed","sad","upset"],char:"🙁",fitzpatrick_scale:false,category:"people"},frowning_face:{keywords:["face","sad","upset","frown"],char:"☹",fitzpatrick_scale:false,category:"people"},persevere:{keywords:["face","sick","no","upset","oops"],char:"😣",fitzpatrick_scale:false,category:"people"},confounded:{keywords:["face","confused","sick","unwell","oops",":S"],char:"😖",fitzpatrick_scale:false,category:"people"},tired_face:{keywords:["sick","whine","upset","frustrated"],char:"😫",fitzpatrick_scale:false,category:"people"},weary:{keywords:["face","tired","sleepy","sad","frustrated","upset"],char:"😩",fitzpatrick_scale:false,category:"people"},pleading:{keywords:["face","begging","mercy"],char:"🥺",fitzpatrick_scale:false,category:"people"},triumph:{keywords:["face","gas","phew","proud","pride"],char:"😤",fitzpatrick_scale:false,category:"people"},open_mouth:{keywords:["face","surprise","impressed","wow","whoa",":O"],char:"😮",fitzpatrick_scale:false,category:"people"},scream:{keywords:["face","munch","scared","omg"],char:"😱",fitzpatrick_scale:false,category:"people"},fearful:{keywords:["face","scared","terrified","nervous","oops","huh"],char:"😨",fitzpatrick_scale:false,category:"people"},cold_sweat:{keywords:["face","nervous","sweat"],char:"😰",fitzpatrick_scale:false,category:"people"},hushed:{keywords:["face","woo","shh"],char:"😯",fitzpatrick_scale:false,category:"people"},frowning:{keywords:["face","aw","what"],char:"😦",fitzpatrick_scale:false,category:"people"},anguished:{keywords:["face","stunned","nervous"],char:"😧",fitzpatrick_scale:false,category:"people"},cry:{keywords:["face","tears","sad","depressed","upset",":'("],char:"😢",fitzpatrick_scale:false,category:"people"},disappointed_relieved:{keywords:["face","phew","sweat","nervous"],char:"😥",fitzpatrick_scale:false,category:"people"},drooling_face:{keywords:["face"],char:"🤤",fitzpatrick_scale:false,category:"people"},sleepy:{keywords:["face","tired","rest","nap"],char:"😪",fitzpatrick_scale:false,category:"people"},sweat:{keywords:["face","hot","sad","tired","exercise"],char:"😓",fitzpatrick_scale:false,category:"people"},hot:{keywords:["face","feverish","heat","red","sweating"],char:"🥵",fitzpatrick_scale:false,category:"people"},cold:{keywords:["face","blue","freezing","frozen","frostbite","icicles"],char:"🥶",fitzpatrick_scale:false,category:"people"},sob:{keywords:["face","cry","tears","sad","upset","depressed"],char:"😭",fitzpatrick_scale:false,category:"people"},dizzy_face:{keywords:["spent","unconscious","xox","dizzy"],char:"😵",fitzpatrick_scale:false,category:"people"},astonished:{keywords:["face","xox","surprised","poisoned"],char:"😲",fitzpatrick_scale:false,category:"people"},zipper_mouth_face:{keywords:["face","sealed","zipper","secret"],char:"🤐",fitzpatrick_scale:false,category:"people"},nauseated_face:{keywords:["face","vomit","gross","green","sick","throw up","ill"],char:"🤢",fitzpatrick_scale:false,category:"people"},sneezing_face:{keywords:["face","gesundheit","sneeze","sick","allergy"],char:"🤧",fitzpatrick_scale:false,category:"people"},vomiting:{keywords:["face","sick"],char:"🤮",fitzpatrick_scale:false,category:"people"},mask:{keywords:["face","sick","ill","disease"],char:"😷",fitzpatrick_scale:false,category:"people"},face_with_thermometer:{keywords:["sick","temperature","thermometer","cold","fever"],char:"🤒",fitzpatrick_scale:false,category:"people"},face_with_head_bandage:{keywords:["injured","clumsy","bandage","hurt"],char:"🤕",fitzpatrick_scale:false,category:"people"},woozy:{keywords:["face","dizzy","intoxicated","tipsy","wavy"],char:"🥴",fitzpatrick_scale:false,category:"people"},sleeping:{keywords:["face","tired","sleepy","night","zzz"],char:"😴",fitzpatrick_scale:false,category:"people"},zzz:{keywords:["sleepy","tired","dream"],char:"💤",fitzpatrick_scale:false,category:"people"},poop:{keywords:["hankey","shitface","fail","turd","shit"],char:"💩",fitzpatrick_scale:false,category:"people"},smiling_imp:{keywords:["devil","horns"],char:"😈",fitzpatrick_scale:false,category:"people"},imp:{keywords:["devil","angry","horns"],char:"👿",fitzpatrick_scale:false,category:"people"},japanese_ogre:{keywords:["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"],char:"👹",fitzpatrick_scale:false,category:"people"},japanese_goblin:{keywords:["red","evil","mask","monster","scary","creepy","japanese","goblin"],char:"👺",fitzpatrick_scale:false,category:"people"},skull:{keywords:["dead","skeleton","creepy","death"],char:"💀",fitzpatrick_scale:false,category:"people"},ghost:{keywords:["halloween","spooky","scary"],char:"👻",fitzpatrick_scale:false,category:"people"},alien:{keywords:["UFO","paul","weird","outer_space"],char:"👽",fitzpatrick_scale:false,category:"people"},robot:{keywords:["computer","machine","bot"],char:"🤖",fitzpatrick_scale:false,category:"people"},smiley_cat:{keywords:["animal","cats","happy","smile"],char:"😺",fitzpatrick_scale:false,category:"people"},smile_cat:{keywords:["animal","cats","smile"],char:"😸",fitzpatrick_scale:false,category:"people"},joy_cat:{keywords:["animal","cats","haha","happy","tears"],char:"😹",fitzpatrick_scale:false,category:"people"},heart_eyes_cat:{keywords:["animal","love","like","affection","cats","valentines","heart"],char:"😻",fitzpatrick_scale:false,category:"people"},smirk_cat:{keywords:["animal","cats","smirk"],char:"😼",fitzpatrick_scale:false,category:"people"},kissing_cat:{keywords:["animal","cats","kiss"],char:"😽",fitzpatrick_scale:false,category:"people"},scream_cat:{keywords:["animal","cats","munch","scared","scream"],char:"🙀",fitzpatrick_scale:false,category:"people"},crying_cat_face:{keywords:["animal","tears","weep","sad","cats","upset","cry"],char:"😿",fitzpatrick_scale:false,category:"people"},pouting_cat:{keywords:["animal","cats"],char:"😾",fitzpatrick_scale:false,category:"people"},palms_up:{keywords:["hands","gesture","cupped","prayer"],char:"🤲",fitzpatrick_scale:true,category:"people"},raised_hands:{keywords:["gesture","hooray","yea","celebration","hands"],char:"🙌",fitzpatrick_scale:true,category:"people"},clap:{keywords:["hands","praise","applause","congrats","yay"],char:"👏",fitzpatrick_scale:true,category:"people"},wave:{keywords:["hands","gesture","goodbye","solong","farewell","hello","hi","palm"],char:"👋",fitzpatrick_scale:true,category:"people"},call_me_hand:{keywords:["hands","gesture"],char:"🤙",fitzpatrick_scale:true,category:"people"},"+1":{keywords:["thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],char:"👍",fitzpatrick_scale:true,category:"people"},"-1":{keywords:["thumbsdown","no","dislike","hand"],char:"👎",fitzpatrick_scale:true,category:"people"},facepunch:{keywords:["angry","violence","fist","hit","attack","hand"],char:"👊",fitzpatrick_scale:true,category:"people"},fist:{keywords:["fingers","hand","grasp"],char:"✊",fitzpatrick_scale:true,category:"people"},fist_left:{keywords:["hand","fistbump"],char:"🤛",fitzpatrick_scale:true,category:"people"},fist_right:{keywords:["hand","fistbump"],char:"🤜",fitzpatrick_scale:true,category:"people"},v:{keywords:["fingers","ohyeah","hand","peace","victory","two"],char:"✌",fitzpatrick_scale:true,category:"people"},ok_hand:{keywords:["fingers","limbs","perfect","ok","okay"],char:"👌",fitzpatrick_scale:true,category:"people"},raised_hand:{keywords:["fingers","stop","highfive","palm","ban"],char:"✋",fitzpatrick_scale:true,category:"people"},raised_back_of_hand:{keywords:["fingers","raised","backhand"],char:"🤚",fitzpatrick_scale:true,category:"people"},open_hands:{keywords:["fingers","butterfly","hands","open"],char:"👐",fitzpatrick_scale:true,category:"people"},muscle:{keywords:["arm","flex","hand","summer","strong","biceps"],char:"💪",fitzpatrick_scale:true,category:"people"},pray:{keywords:["please","hope","wish","namaste","highfive"],char:"🙏",fitzpatrick_scale:true,category:"people"},foot:{keywords:["kick","stomp"],char:"🦶",fitzpatrick_scale:true,category:"people"},leg:{keywords:["kick","limb"],char:"🦵",fitzpatrick_scale:true,category:"people"},handshake:{keywords:["agreement","shake"],char:"🤝",fitzpatrick_scale:false,category:"people"},point_up:{keywords:["hand","fingers","direction","up"],char:"☝",fitzpatrick_scale:true,category:"people"},point_up_2:{keywords:["fingers","hand","direction","up"],char:"👆",fitzpatrick_scale:true,category:"people"},point_down:{keywords:["fingers","hand","direction","down"],char:"👇",fitzpatrick_scale:true,category:"people"},point_left:{keywords:["direction","fingers","hand","left"],char:"👈",fitzpatrick_scale:true,category:"people"},point_right:{keywords:["fingers","hand","direction","right"],char:"👉",fitzpatrick_scale:true,category:"people"},fu:{keywords:["hand","fingers","rude","middle","flipping"],char:"🖕",fitzpatrick_scale:true,category:"people"},raised_hand_with_fingers_splayed:{keywords:["hand","fingers","palm"],char:"🖐",fitzpatrick_scale:true,category:"people"},love_you:{keywords:["hand","fingers","gesture"],char:"🤟",fitzpatrick_scale:true,category:"people"},metal:{keywords:["hand","fingers","evil_eye","sign_of_horns","rock_on"],char:"🤘",fitzpatrick_scale:true,category:"people"},crossed_fingers:{keywords:["good","lucky"],char:"🤞",fitzpatrick_scale:true,category:"people"},vulcan_salute:{keywords:["hand","fingers","spock","star trek"],char:"🖖",fitzpatrick_scale:true,category:"people"},writing_hand:{keywords:["lower_left_ballpoint_pen","stationery","write","compose"],char:"✍",fitzpatrick_scale:true,category:"people"},selfie:{keywords:["camera","phone"],char:"🤳",fitzpatrick_scale:true,category:"people"},nail_care:{keywords:["beauty","manicure","finger","fashion","nail"],char:"💅",fitzpatrick_scale:true,category:"people"},lips:{keywords:["mouth","kiss"],char:"👄",fitzpatrick_scale:false,category:"people"},tooth:{keywords:["teeth","dentist"],char:"🦷",fitzpatrick_scale:false,category:"people"},tongue:{keywords:["mouth","playful"],char:"👅",fitzpatrick_scale:false,category:"people"},ear:{keywords:["face","hear","sound","listen"],char:"👂",fitzpatrick_scale:true,category:"people"},nose:{keywords:["smell","sniff"],char:"👃",fitzpatrick_scale:true,category:"people"},eye:{keywords:["face","look","see","watch","stare"],char:"👁",fitzpatrick_scale:false,category:"people"},eyes:{keywords:["look","watch","stalk","peek","see"],char:"👀",fitzpatrick_scale:false,category:"people"},brain:{keywords:["smart","intelligent"],char:"🧠",fitzpatrick_scale:false,category:"people"},bust_in_silhouette:{keywords:["user","person","human"],char:"👤",fitzpatrick_scale:false,category:"people"},busts_in_silhouette:{keywords:["user","person","human","group","team"],char:"👥",fitzpatrick_scale:false,category:"people"},speaking_head:{keywords:["user","person","human","sing","say","talk"],char:"🗣",fitzpatrick_scale:false,category:"people"},baby:{keywords:["child","boy","girl","toddler"],char:"👶",fitzpatrick_scale:true,category:"people"},child:{keywords:["gender-neutral","young"],char:"🧒",fitzpatrick_scale:true,category:"people"},boy:{keywords:["man","male","guy","teenager"],char:"👦",fitzpatrick_scale:true,category:"people"},girl:{keywords:["female","woman","teenager"],char:"👧",fitzpatrick_scale:true,category:"people"},adult:{keywords:["gender-neutral","person"],char:"🧑",fitzpatrick_scale:true,category:"people"},man:{keywords:["mustache","father","dad","guy","classy","sir","moustache"],char:"👨",fitzpatrick_scale:true,category:"people"},woman:{keywords:["female","girls","lady"],char:"👩",fitzpatrick_scale:true,category:"people"},blonde_woman:{keywords:["woman","female","girl","blonde","person"],char:"👱‍♀️",fitzpatrick_scale:true,category:"people"},blonde_man:{keywords:["man","male","boy","blonde","guy","person"],char:"👱",fitzpatrick_scale:true,category:"people"},bearded_person:{keywords:["person","bewhiskered"],char:"🧔",fitzpatrick_scale:true,category:"people"},older_adult:{keywords:["human","elder","senior","gender-neutral"],char:"🧓",fitzpatrick_scale:true,category:"people"},older_man:{keywords:["human","male","men","old","elder","senior"],char:"👴",fitzpatrick_scale:true,category:"people"},older_woman:{keywords:["human","female","women","lady","old","elder","senior"],char:"👵",fitzpatrick_scale:true,category:"people"},man_with_gua_pi_mao:{keywords:["male","boy","chinese"],char:"👲",fitzpatrick_scale:true,category:"people"},woman_with_headscarf:{keywords:["female","hijab","mantilla","tichel"],char:"🧕",fitzpatrick_scale:true,category:"people"},woman_with_turban:{keywords:["female","indian","hinduism","arabs","woman"],char:"👳‍♀️",fitzpatrick_scale:true,category:"people"},man_with_turban:{keywords:["male","indian","hinduism","arabs"],char:"👳",fitzpatrick_scale:true,category:"people"},policewoman:{keywords:["woman","police","law","legal","enforcement","arrest","911","female"],char:"👮‍♀️",fitzpatrick_scale:true,category:"people"},policeman:{keywords:["man","police","law","legal","enforcement","arrest","911"],char:"👮",fitzpatrick_scale:true,category:"people"},construction_worker_woman:{keywords:["female","human","wip","build","construction","worker","labor","woman"],char:"👷‍♀️",fitzpatrick_scale:true,category:"people"},construction_worker_man:{keywords:["male","human","wip","guy","build","construction","worker","labor"],char:"👷",fitzpatrick_scale:true,category:"people"},guardswoman:{keywords:["uk","gb","british","female","royal","woman"],char:"💂‍♀️",fitzpatrick_scale:true,category:"people"},guardsman:{keywords:["uk","gb","british","male","guy","royal"],char:"💂",fitzpatrick_scale:true,category:"people"},female_detective:{keywords:["human","spy","detective","female","woman"],char:"🕵️‍♀️",fitzpatrick_scale:true,category:"people"},male_detective:{keywords:["human","spy","detective"],char:"🕵",fitzpatrick_scale:true,category:"people"},woman_health_worker:{keywords:["doctor","nurse","therapist","healthcare","woman","human"],char:"👩‍⚕️",fitzpatrick_scale:true,category:"people"},man_health_worker:{keywords:["doctor","nurse","therapist","healthcare","man","human"],char:"👨‍⚕️",fitzpatrick_scale:true,category:"people"},woman_farmer:{keywords:["rancher","gardener","woman","human"],char:"👩‍🌾",fitzpatrick_scale:true,category:"people"},man_farmer:{keywords:["rancher","gardener","man","human"],char:"👨‍🌾",fitzpatrick_scale:true,category:"people"},woman_cook:{keywords:["chef","woman","human"],char:"👩‍🍳",fitzpatrick_scale:true,category:"people"},man_cook:{keywords:["chef","man","human"],char:"👨‍🍳",fitzpatrick_scale:true,category:"people"},woman_student:{keywords:["graduate","woman","human"],char:"👩‍🎓",fitzpatrick_scale:true,category:"people"},man_student:{keywords:["graduate","man","human"],char:"👨‍🎓",fitzpatrick_scale:true,category:"people"},woman_singer:{keywords:["rockstar","entertainer","woman","human"],char:"👩‍🎤",fitzpatrick_scale:true,category:"people"},man_singer:{keywords:["rockstar","entertainer","man","human"],char:"👨‍🎤",fitzpatrick_scale:true,category:"people"},woman_teacher:{keywords:["instructor","professor","woman","human"],char:"👩‍🏫",fitzpatrick_scale:true,category:"people"},man_teacher:{keywords:["instructor","professor","man","human"],char:"👨‍🏫",fitzpatrick_scale:true,category:"people"},woman_factory_worker:{keywords:["assembly","industrial","woman","human"],char:"👩‍🏭",fitzpatrick_scale:true,category:"people"},man_factory_worker:{keywords:["assembly","industrial","man","human"],char:"👨‍🏭",fitzpatrick_scale:true,category:"people"},woman_technologist:{keywords:["coder","developer","engineer","programmer","software","woman","human","laptop","computer"],char:"👩‍💻",fitzpatrick_scale:true,category:"people"},man_technologist:{keywords:["coder","developer","engineer","programmer","software","man","human","laptop","computer"],char:"👨‍💻",fitzpatrick_scale:true,category:"people"},woman_office_worker:{keywords:["business","manager","woman","human"],char:"👩‍💼",fitzpatrick_scale:true,category:"people"},man_office_worker:{keywords:["business","manager","man","human"],char:"👨‍💼",fitzpatrick_scale:true,category:"people"},woman_mechanic:{keywords:["plumber","woman","human","wrench"],char:"👩‍🔧",fitzpatrick_scale:true,category:"people"},man_mechanic:{keywords:["plumber","man","human","wrench"],char:"👨‍🔧",fitzpatrick_scale:true,category:"people"},woman_scientist:{keywords:["biologist","chemist","engineer","physicist","woman","human"],char:"👩‍🔬",fitzpatrick_scale:true,category:"people"},man_scientist:{keywords:["biologist","chemist","engineer","physicist","man","human"],char:"👨‍🔬",fitzpatrick_scale:true,category:"people"},woman_artist:{keywords:["painter","woman","human"],char:"👩‍🎨",fitzpatrick_scale:true,category:"people"},man_artist:{keywords:["painter","man","human"],char:"👨‍🎨",fitzpatrick_scale:true,category:"people"},woman_firefighter:{keywords:["fireman","woman","human"],char:"👩‍🚒",fitzpatrick_scale:true,category:"people"},man_firefighter:{keywords:["fireman","man","human"],char:"👨‍🚒",fitzpatrick_scale:true,category:"people"},woman_pilot:{keywords:["aviator","plane","woman","human"],char:"👩‍✈️",fitzpatrick_scale:true,category:"people"},man_pilot:{keywords:["aviator","plane","man","human"],char:"👨‍✈️",fitzpatrick_scale:true,category:"people"},woman_astronaut:{keywords:["space","rocket","woman","human"],char:"👩‍🚀",fitzpatrick_scale:true,category:"people"},man_astronaut:{keywords:["space","rocket","man","human"],char:"👨‍🚀",fitzpatrick_scale:true,category:"people"},woman_judge:{keywords:["justice","court","woman","human"],char:"👩‍⚖️",fitzpatrick_scale:true,category:"people"},man_judge:{keywords:["justice","court","man","human"],char:"👨‍⚖️",fitzpatrick_scale:true,category:"people"},woman_superhero:{keywords:["woman","female","good","heroine","superpowers"],char:"🦸‍♀️",fitzpatrick_scale:true,category:"people"},man_superhero:{keywords:["man","male","good","hero","superpowers"],char:"🦸‍♂️",fitzpatrick_scale:true,category:"people"},woman_supervillain:{keywords:["woman","female","evil","bad","criminal","heroine","superpowers"],char:"🦹‍♀️",fitzpatrick_scale:true,category:"people"},man_supervillain:{keywords:["man","male","evil","bad","criminal","hero","superpowers"],char:"🦹‍♂️",fitzpatrick_scale:true,category:"people"},mrs_claus:{keywords:["woman","female","xmas","mother christmas"],char:"🤶",fitzpatrick_scale:true,category:"people"},santa:{keywords:["festival","man","male","xmas","father christmas"],char:"🎅",fitzpatrick_scale:true,category:"people"},sorceress:{keywords:["woman","female","mage","witch"],char:"🧙‍♀️",fitzpatrick_scale:true,category:"people"},wizard:{keywords:["man","male","mage","sorcerer"],char:"🧙‍♂️",fitzpatrick_scale:true,category:"people"},woman_elf:{keywords:["woman","female"],char:"🧝‍♀️",fitzpatrick_scale:true,category:"people"},man_elf:{keywords:["man","male"],char:"🧝‍♂️",fitzpatrick_scale:true,category:"people"},woman_vampire:{keywords:["woman","female"],char:"🧛‍♀️",fitzpatrick_scale:true,category:"people"},man_vampire:{keywords:["man","male","dracula"],char:"🧛‍♂️",fitzpatrick_scale:true,category:"people"},woman_zombie:{keywords:["woman","female","undead","walking dead"],char:"🧟‍♀️",fitzpatrick_scale:false,category:"people"},man_zombie:{keywords:["man","male","dracula","undead","walking dead"],char:"🧟‍♂️",fitzpatrick_scale:false,category:"people"},woman_genie:{keywords:["woman","female"],char:"🧞‍♀️",fitzpatrick_scale:false,category:"people"},man_genie:{keywords:["man","male"],char:"🧞‍♂️",fitzpatrick_scale:false,category:"people"},mermaid:{keywords:["woman","female","merwoman","ariel"],char:"🧜‍♀️",fitzpatrick_scale:true,category:"people"},merman:{keywords:["man","male","triton"],char:"🧜‍♂️",fitzpatrick_scale:true,category:"people"},woman_fairy:{keywords:["woman","female"],char:"🧚‍♀️",fitzpatrick_scale:true,category:"people"},man_fairy:{keywords:["man","male"],char:"🧚‍♂️",fitzpatrick_scale:true,category:"people"},angel:{keywords:["heaven","wings","halo"],char:"👼",fitzpatrick_scale:true,category:"people"},pregnant_woman:{keywords:["baby"],char:"🤰",fitzpatrick_scale:true,category:"people"},breastfeeding:{keywords:["nursing","baby"],char:"🤱",fitzpatrick_scale:true,category:"people"},princess:{keywords:["girl","woman","female","blond","crown","royal","queen"],char:"👸",fitzpatrick_scale:true,category:"people"},prince:{keywords:["boy","man","male","crown","royal","king"],char:"🤴",fitzpatrick_scale:true,category:"people"},bride_with_veil:{keywords:["couple","marriage","wedding","woman","bride"],char:"👰",fitzpatrick_scale:true,category:"people"},man_in_tuxedo:{keywords:["couple","marriage","wedding","groom"],char:"🤵",fitzpatrick_scale:true,category:"people"},running_woman:{keywords:["woman","walking","exercise","race","running","female"],char:"🏃‍♀️",fitzpatrick_scale:true,category:"people"},running_man:{keywords:["man","walking","exercise","race","running"],char:"🏃",fitzpatrick_scale:true,category:"people"},walking_woman:{keywords:["human","feet","steps","woman","female"],char:"🚶‍♀️",fitzpatrick_scale:true,category:"people"},walking_man:{keywords:["human","feet","steps"],char:"🚶",fitzpatrick_scale:true,category:"people"},dancer:{keywords:["female","girl","woman","fun"],char:"💃",fitzpatrick_scale:true,category:"people"},man_dancing:{keywords:["male","boy","fun","dancer"],char:"🕺",fitzpatrick_scale:true,category:"people"},dancing_women:{keywords:["female","bunny","women","girls"],char:"👯",fitzpatrick_scale:false,category:"people"},dancing_men:{keywords:["male","bunny","men","boys"],char:"👯‍♂️",fitzpatrick_scale:false,category:"people"},couple:{keywords:["pair","people","human","love","date","dating","like","affection","valentines","marriage"],char:"👫",fitzpatrick_scale:false,category:"people"},two_men_holding_hands:{keywords:["pair","couple","love","like","bromance","friendship","people","human"],char:"👬",fitzpatrick_scale:false,category:"people"},two_women_holding_hands:{keywords:["pair","friendship","couple","love","like","female","people","human"],char:"👭",fitzpatrick_scale:false,category:"people"},bowing_woman:{keywords:["woman","female","girl"],char:"🙇‍♀️",fitzpatrick_scale:true,category:"people"},bowing_man:{keywords:["man","male","boy"],char:"🙇",fitzpatrick_scale:true,category:"people"},man_facepalming:{keywords:["man","male","boy","disbelief"],char:"🤦‍♂️",fitzpatrick_scale:true,category:"people"},woman_facepalming:{keywords:["woman","female","girl","disbelief"],char:"🤦‍♀️",fitzpatrick_scale:true,category:"people"},woman_shrugging:{keywords:["woman","female","girl","confused","indifferent","doubt"],char:"🤷",fitzpatrick_scale:true,category:"people"},man_shrugging:{keywords:["man","male","boy","confused","indifferent","doubt"],char:"🤷‍♂️",fitzpatrick_scale:true,category:"people"},tipping_hand_woman:{keywords:["female","girl","woman","human","information"],char:"💁",fitzpatrick_scale:true,category:"people"},tipping_hand_man:{keywords:["male","boy","man","human","information"],char:"💁‍♂️",fitzpatrick_scale:true,category:"people"},no_good_woman:{keywords:["female","girl","woman","nope"],char:"🙅",fitzpatrick_scale:true,category:"people"},no_good_man:{keywords:["male","boy","man","nope"],char:"🙅‍♂️",fitzpatrick_scale:true,category:"people"},ok_woman:{keywords:["women","girl","female","pink","human","woman"],char:"🙆",fitzpatrick_scale:true,category:"people"},ok_man:{keywords:["men","boy","male","blue","human","man"],char:"🙆‍♂️",fitzpatrick_scale:true,category:"people"},raising_hand_woman:{keywords:["female","girl","woman"],char:"🙋",fitzpatrick_scale:true,category:"people"},raising_hand_man:{keywords:["male","boy","man"],char:"🙋‍♂️",fitzpatrick_scale:true,category:"people"},pouting_woman:{keywords:["female","girl","woman"],char:"🙎",fitzpatrick_scale:true,category:"people"},pouting_man:{keywords:["male","boy","man"],char:"🙎‍♂️",fitzpatrick_scale:true,category:"people"},frowning_woman:{keywords:["female","girl","woman","sad","depressed","discouraged","unhappy"],char:"🙍",fitzpatrick_scale:true,category:"people"},frowning_man:{keywords:["male","boy","man","sad","depressed","discouraged","unhappy"],char:"🙍‍♂️",fitzpatrick_scale:true,category:"people"},haircut_woman:{keywords:["female","girl","woman"],char:"💇",fitzpatrick_scale:true,category:"people"},haircut_man:{keywords:["male","boy","man"],char:"💇‍♂️",fitzpatrick_scale:true,category:"people"},massage_woman:{keywords:["female","girl","woman","head"],char:"💆",fitzpatrick_scale:true,category:"people"},massage_man:{keywords:["male","boy","man","head"],char:"💆‍♂️",fitzpatrick_scale:true,category:"people"},woman_in_steamy_room:{keywords:["female","woman","spa","steamroom","sauna"],char:"🧖‍♀️",fitzpatrick_scale:true,category:"people"},man_in_steamy_room:{keywords:["male","man","spa","steamroom","sauna"],char:"🧖‍♂️",fitzpatrick_scale:true,category:"people"},couple_with_heart_woman_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"💑",fitzpatrick_scale:false,category:"people"},couple_with_heart_woman_woman:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"👩‍❤️‍👩",fitzpatrick_scale:false,category:"people"},couple_with_heart_man_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"👨‍❤️‍👨",fitzpatrick_scale:false,category:"people"},couplekiss_man_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:"💏",fitzpatrick_scale:false,category:"people"},couplekiss_woman_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:"👩‍❤️‍💋‍👩",fitzpatrick_scale:false,category:"people"},couplekiss_man_man:{keywords:["pair","valentines","love","like","dating","marriage"],char:"👨‍❤️‍💋‍👨",fitzpatrick_scale:false,category:"people"},family_man_woman_boy:{keywords:["home","parents","child","mom","dad","father","mother","people","human"],char:"👪",fitzpatrick_scale:false,category:"people"},family_man_woman_girl:{keywords:["home","parents","people","human","child"],char:"👨‍👩‍👧",fitzpatrick_scale:false,category:"people"},family_man_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👩‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_man_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👩‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_man_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:"👨‍👩‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_woman_woman_boy:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👦",fitzpatrick_scale:false,category:"people"},family_woman_woman_girl:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👧",fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_woman_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_woman_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:"👩‍👩‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_man_man_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👦",fitzpatrick_scale:false,category:"people"},family_man_man_girl:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👧",fitzpatrick_scale:false,category:"people"},family_man_man_girl_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_man_man_boy_boy:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_man_man_girl_girl:{keywords:["home","parents","people","human","children"],char:"👨‍👨‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_woman_boy:{keywords:["home","parent","people","human","child"],char:"👩‍👦",fitzpatrick_scale:false,category:"people"},family_woman_girl:{keywords:["home","parent","people","human","child"],char:"👩‍👧",fitzpatrick_scale:false,category:"people"},family_woman_girl_boy:{keywords:["home","parent","people","human","children"],char:"👩‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_woman_boy_boy:{keywords:["home","parent","people","human","children"],char:"👩‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_woman_girl_girl:{keywords:["home","parent","people","human","children"],char:"👩‍👧‍👧",fitzpatrick_scale:false,category:"people"},family_man_boy:{keywords:["home","parent","people","human","child"],char:"👨‍👦",fitzpatrick_scale:false,category:"people"},family_man_girl:{keywords:["home","parent","people","human","child"],char:"👨‍👧",fitzpatrick_scale:false,category:"people"},family_man_girl_boy:{keywords:["home","parent","people","human","children"],char:"👨‍👧‍👦",fitzpatrick_scale:false,category:"people"},family_man_boy_boy:{keywords:["home","parent","people","human","children"],char:"👨‍👦‍👦",fitzpatrick_scale:false,category:"people"},family_man_girl_girl:{keywords:["home","parent","people","human","children"],char:"👨‍👧‍👧",fitzpatrick_scale:false,category:"people"},yarn:{keywords:["ball","crochet","knit"],char:"🧶",fitzpatrick_scale:false,category:"people"},thread:{keywords:["needle","sewing","spool","string"],char:"🧵",fitzpatrick_scale:false,category:"people"},coat:{keywords:["jacket"],char:"🧥",fitzpatrick_scale:false,category:"people"},labcoat:{keywords:["doctor","experiment","scientist","chemist"],char:"🥼",fitzpatrick_scale:false,category:"people"},womans_clothes:{keywords:["fashion","shopping_bags","female"],char:"👚",fitzpatrick_scale:false,category:"people"},tshirt:{keywords:["fashion","cloth","casual","shirt","tee"],char:"👕",fitzpatrick_scale:false,category:"people"},jeans:{keywords:["fashion","shopping"],char:"👖",fitzpatrick_scale:false,category:"people"},necktie:{keywords:["shirt","suitup","formal","fashion","cloth","business"],char:"👔",fitzpatrick_scale:false,category:"people"},dress:{keywords:["clothes","fashion","shopping"],char:"👗",fitzpatrick_scale:false,category:"people"},bikini:{keywords:["swimming","female","woman","girl","fashion","beach","summer"],char:"👙",fitzpatrick_scale:false,category:"people"},kimono:{keywords:["dress","fashion","women","female","japanese"],char:"👘",fitzpatrick_scale:false,category:"people"},lipstick:{keywords:["female","girl","fashion","woman"],char:"💄",fitzpatrick_scale:false,category:"people"},kiss:{keywords:["face","lips","love","like","affection","valentines"],char:"💋",fitzpatrick_scale:false,category:"people"},footprints:{keywords:["feet","tracking","walking","beach"],char:"👣",fitzpatrick_scale:false,category:"people"},flat_shoe:{keywords:["ballet","slip-on","slipper"],char:"🥿",fitzpatrick_scale:false,category:"people"},high_heel:{keywords:["fashion","shoes","female","pumps","stiletto"],char:"👠",fitzpatrick_scale:false,category:"people"},sandal:{keywords:["shoes","fashion","flip flops"],char:"👡",fitzpatrick_scale:false,category:"people"},boot:{keywords:["shoes","fashion"],char:"👢",fitzpatrick_scale:false,category:"people"},mans_shoe:{keywords:["fashion","male"],char:"👞",fitzpatrick_scale:false,category:"people"},athletic_shoe:{keywords:["shoes","sports","sneakers"],char:"👟",fitzpatrick_scale:false,category:"people"},hiking_boot:{keywords:["backpacking","camping","hiking"],char:"🥾",fitzpatrick_scale:false,category:"people"},socks:{keywords:["stockings","clothes"],char:"🧦",fitzpatrick_scale:false,category:"people"},gloves:{keywords:["hands","winter","clothes"],char:"🧤",fitzpatrick_scale:false,category:"people"},scarf:{keywords:["neck","winter","clothes"],char:"🧣",fitzpatrick_scale:false,category:"people"},womans_hat:{keywords:["fashion","accessories","female","lady","spring"],char:"👒",fitzpatrick_scale:false,category:"people"},tophat:{keywords:["magic","gentleman","classy","circus"],char:"🎩",fitzpatrick_scale:false,category:"people"},billed_hat:{keywords:["cap","baseball"],char:"🧢",fitzpatrick_scale:false,category:"people"},rescue_worker_helmet:{keywords:["construction","build"],char:"⛑",fitzpatrick_scale:false,category:"people"},mortar_board:{keywords:["school","college","degree","university","graduation","cap","hat","legal","learn","education"],char:"🎓",fitzpatrick_scale:false,category:"people"},crown:{keywords:["king","kod","leader","royalty","lord"],char:"👑",fitzpatrick_scale:false,category:"people"},school_satchel:{keywords:["student","education","bag","backpack"],char:"🎒",fitzpatrick_scale:false,category:"people"},luggage:{keywords:["packing","travel"],char:"🧳",fitzpatrick_scale:false,category:"people"},pouch:{keywords:["bag","accessories","shopping"],char:"👝",fitzpatrick_scale:false,category:"people"},purse:{keywords:["fashion","accessories","money","sales","shopping"],char:"👛",fitzpatrick_scale:false,category:"people"},handbag:{keywords:["fashion","accessory","accessories","shopping"],char:"👜",fitzpatrick_scale:false,category:"people"},briefcase:{keywords:["business","documents","work","law","legal","job","career"],char:"💼",fitzpatrick_scale:false,category:"people"},eyeglasses:{keywords:["fashion","accessories","eyesight","nerdy","dork","geek"],char:"👓",fitzpatrick_scale:false,category:"people"},dark_sunglasses:{keywords:["face","cool","accessories"],char:"🕶",fitzpatrick_scale:false,category:"people"},goggles:{keywords:["eyes","protection","safety"],char:"🥽",fitzpatrick_scale:false,category:"people"},ring:{keywords:["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"],char:"💍",fitzpatrick_scale:false,category:"people"},closed_umbrella:{keywords:["weather","rain","drizzle"],char:"🌂",fitzpatrick_scale:false,category:"people"},dog:{keywords:["animal","friend","nature","woof","puppy","pet","faithful"],char:"🐶",fitzpatrick_scale:false,category:"animals_and_nature"},cat:{keywords:["animal","meow","nature","pet","kitten"],char:"🐱",fitzpatrick_scale:false,category:"animals_and_nature"},mouse:{keywords:["animal","nature","cheese_wedge","rodent"],char:"🐭",fitzpatrick_scale:false,category:"animals_and_nature"},hamster:{keywords:["animal","nature"],char:"🐹",fitzpatrick_scale:false,category:"animals_and_nature"},rabbit:{keywords:["animal","nature","pet","spring","magic","bunny"],char:"🐰",fitzpatrick_scale:false,category:"animals_and_nature"},fox_face:{keywords:["animal","nature","face"],char:"🦊",fitzpatrick_scale:false,category:"animals_and_nature"},bear:{keywords:["animal","nature","wild"],char:"🐻",fitzpatrick_scale:false,category:"animals_and_nature"},panda_face:{keywords:["animal","nature","panda"],char:"🐼",fitzpatrick_scale:false,category:"animals_and_nature"},koala:{keywords:["animal","nature"],char:"🐨",fitzpatrick_scale:false,category:"animals_and_nature"},tiger:{keywords:["animal","cat","danger","wild","nature","roar"],char:"🐯",fitzpatrick_scale:false,category:"animals_and_nature"},lion:{keywords:["animal","nature"],char:"🦁",fitzpatrick_scale:false,category:"animals_and_nature"},cow:{keywords:["beef","ox","animal","nature","moo","milk"],char:"🐮",fitzpatrick_scale:false,category:"animals_and_nature"},pig:{keywords:["animal","oink","nature"],char:"🐷",fitzpatrick_scale:false,category:"animals_and_nature"},pig_nose:{keywords:["animal","oink"],char:"🐽",fitzpatrick_scale:false,category:"animals_and_nature"},frog:{keywords:["animal","nature","croak","toad"],char:"🐸",fitzpatrick_scale:false,category:"animals_and_nature"},squid:{keywords:["animal","nature","ocean","sea"],char:"🦑",fitzpatrick_scale:false,category:"animals_and_nature"},octopus:{keywords:["animal","creature","ocean","sea","nature","beach"],char:"🐙",fitzpatrick_scale:false,category:"animals_and_nature"},shrimp:{keywords:["animal","ocean","nature","seafood"],char:"🦐",fitzpatrick_scale:false,category:"animals_and_nature"},monkey_face:{keywords:["animal","nature","circus"],char:"🐵",fitzpatrick_scale:false,category:"animals_and_nature"},gorilla:{keywords:["animal","nature","circus"],char:"🦍",fitzpatrick_scale:false,category:"animals_and_nature"},see_no_evil:{keywords:["monkey","animal","nature","haha"],char:"🙈",fitzpatrick_scale:false,category:"animals_and_nature"},hear_no_evil:{keywords:["animal","monkey","nature"],char:"🙉",fitzpatrick_scale:false,category:"animals_and_nature"},speak_no_evil:{keywords:["monkey","animal","nature","omg"],char:"🙊",fitzpatrick_scale:false,category:"animals_and_nature"},monkey:{keywords:["animal","nature","banana","circus"],char:"🐒",fitzpatrick_scale:false,category:"animals_and_nature"},chicken:{keywords:["animal","cluck","nature","bird"],char:"🐔",fitzpatrick_scale:false,category:"animals_and_nature"},penguin:{keywords:["animal","nature"],char:"🐧",fitzpatrick_scale:false,category:"animals_and_nature"},bird:{keywords:["animal","nature","fly","tweet","spring"],char:"🐦",fitzpatrick_scale:false,category:"animals_and_nature"},baby_chick:{keywords:["animal","chicken","bird"],char:"🐤",fitzpatrick_scale:false,category:"animals_and_nature"},hatching_chick:{keywords:["animal","chicken","egg","born","baby","bird"],char:"🐣",fitzpatrick_scale:false,category:"animals_and_nature"},hatched_chick:{keywords:["animal","chicken","baby","bird"],char:"🐥",fitzpatrick_scale:false,category:"animals_and_nature"},duck:{keywords:["animal","nature","bird","mallard"],char:"🦆",fitzpatrick_scale:false,category:"animals_and_nature"},eagle:{keywords:["animal","nature","bird"],char:"🦅",fitzpatrick_scale:false,category:"animals_and_nature"},owl:{keywords:["animal","nature","bird","hoot"],char:"🦉",fitzpatrick_scale:false,category:"animals_and_nature"},bat:{keywords:["animal","nature","blind","vampire"],char:"🦇",fitzpatrick_scale:false,category:"animals_and_nature"},wolf:{keywords:["animal","nature","wild"],char:"🐺",fitzpatrick_scale:false,category:"animals_and_nature"},boar:{keywords:["animal","nature"],char:"🐗",fitzpatrick_scale:false,category:"animals_and_nature"},horse:{keywords:["animal","brown","nature"],char:"🐴",fitzpatrick_scale:false,category:"animals_and_nature"},unicorn:{keywords:["animal","nature","mystical"],char:"🦄",fitzpatrick_scale:false,category:"animals_and_nature"},honeybee:{keywords:["animal","insect","nature","bug","spring","honey"],char:"🐝",fitzpatrick_scale:false,category:"animals_and_nature"},bug:{keywords:["animal","insect","nature","worm"],char:"🐛",fitzpatrick_scale:false,category:"animals_and_nature"},butterfly:{keywords:["animal","insect","nature","caterpillar"],char:"🦋",fitzpatrick_scale:false,category:"animals_and_nature"},snail:{keywords:["slow","animal","shell"],char:"🐌",fitzpatrick_scale:false,category:"animals_and_nature"},beetle:{keywords:["animal","insect","nature","ladybug"],char:"🐞",fitzpatrick_scale:false,category:"animals_and_nature"},ant:{keywords:["animal","insect","nature","bug"],char:"🐜",fitzpatrick_scale:false,category:"animals_and_nature"},grasshopper:{keywords:["animal","cricket","chirp"],char:"🦗",fitzpatrick_scale:false,category:"animals_and_nature"},spider:{keywords:["animal","arachnid"],char:"🕷",fitzpatrick_scale:false,category:"animals_and_nature"},scorpion:{keywords:["animal","arachnid"],char:"🦂",fitzpatrick_scale:false,category:"animals_and_nature"},crab:{keywords:["animal","crustacean"],char:"🦀",fitzpatrick_scale:false,category:"animals_and_nature"},snake:{keywords:["animal","evil","nature","hiss","python"],char:"🐍",fitzpatrick_scale:false,category:"animals_and_nature"},lizard:{keywords:["animal","nature","reptile"],char:"🦎",fitzpatrick_scale:false,category:"animals_and_nature"},"t-rex":{keywords:["animal","nature","dinosaur","tyrannosaurus","extinct"],char:"🦖",fitzpatrick_scale:false,category:"animals_and_nature"},sauropod:{keywords:["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"],char:"🦕",fitzpatrick_scale:false,category:"animals_and_nature"},turtle:{keywords:["animal","slow","nature","tortoise"],char:"🐢",fitzpatrick_scale:false,category:"animals_and_nature"},tropical_fish:{keywords:["animal","swim","ocean","beach","nemo"],char:"🐠",fitzpatrick_scale:false,category:"animals_and_nature"},fish:{keywords:["animal","food","nature"],char:"🐟",fitzpatrick_scale:false,category:"animals_and_nature"},blowfish:{keywords:["animal","nature","food","sea","ocean"],char:"🐡",fitzpatrick_scale:false,category:"animals_and_nature"},dolphin:{keywords:["animal","nature","fish","sea","ocean","flipper","fins","beach"],char:"🐬",fitzpatrick_scale:false,category:"animals_and_nature"},shark:{keywords:["animal","nature","fish","sea","ocean","jaws","fins","beach"],char:"🦈",fitzpatrick_scale:false,category:"animals_and_nature"},whale:{keywords:["animal","nature","sea","ocean"],char:"🐳",fitzpatrick_scale:false,category:"animals_and_nature"},whale2:{keywords:["animal","nature","sea","ocean"],char:"🐋",fitzpatrick_scale:false,category:"animals_and_nature"},crocodile:{keywords:["animal","nature","reptile","lizard","alligator"],char:"🐊",fitzpatrick_scale:false,category:"animals_and_nature"},leopard:{keywords:["animal","nature"],char:"🐆",fitzpatrick_scale:false,category:"animals_and_nature"},zebra:{keywords:["animal","nature","stripes","safari"],char:"🦓",fitzpatrick_scale:false,category:"animals_and_nature"},tiger2:{keywords:["animal","nature","roar"],char:"🐅",fitzpatrick_scale:false,category:"animals_and_nature"},water_buffalo:{keywords:["animal","nature","ox","cow"],char:"🐃",fitzpatrick_scale:false,category:"animals_and_nature"},ox:{keywords:["animal","cow","beef"],char:"🐂",fitzpatrick_scale:false,category:"animals_and_nature"},cow2:{keywords:["beef","ox","animal","nature","moo","milk"],char:"🐄",fitzpatrick_scale:false,category:"animals_and_nature"},deer:{keywords:["animal","nature","horns","venison"],char:"🦌",fitzpatrick_scale:false,category:"animals_and_nature"},dromedary_camel:{keywords:["animal","hot","desert","hump"],char:"🐪",fitzpatrick_scale:false,category:"animals_and_nature"},camel:{keywords:["animal","nature","hot","desert","hump"],char:"🐫",fitzpatrick_scale:false,category:"animals_and_nature"},giraffe:{keywords:["animal","nature","spots","safari"],char:"🦒",fitzpatrick_scale:false,category:"animals_and_nature"},elephant:{keywords:["animal","nature","nose","th","circus"],char:"🐘",fitzpatrick_scale:false,category:"animals_and_nature"},rhinoceros:{keywords:["animal","nature","horn"],char:"🦏",fitzpatrick_scale:false,category:"animals_and_nature"},goat:{keywords:["animal","nature"],char:"🐐",fitzpatrick_scale:false,category:"animals_and_nature"},ram:{keywords:["animal","sheep","nature"],char:"🐏",fitzpatrick_scale:false,category:"animals_and_nature"},sheep:{keywords:["animal","nature","wool","shipit"],char:"🐑",fitzpatrick_scale:false,category:"animals_and_nature"},racehorse:{keywords:["animal","gamble","luck"],char:"🐎",fitzpatrick_scale:false,category:"animals_and_nature"},pig2:{keywords:["animal","nature"],char:"🐖",fitzpatrick_scale:false,category:"animals_and_nature"},rat:{keywords:["animal","mouse","rodent"],char:"🐀",fitzpatrick_scale:false,category:"animals_and_nature"},mouse2:{keywords:["animal","nature","rodent"],char:"🐁",fitzpatrick_scale:false,category:"animals_and_nature"},rooster:{keywords:["animal","nature","chicken"],char:"🐓",fitzpatrick_scale:false,category:"animals_and_nature"},turkey:{keywords:["animal","bird"],char:"🦃",fitzpatrick_scale:false,category:"animals_and_nature"},dove:{keywords:["animal","bird"],char:"🕊",fitzpatrick_scale:false,category:"animals_and_nature"},dog2:{keywords:["animal","nature","friend","doge","pet","faithful"],char:"🐕",fitzpatrick_scale:false,category:"animals_and_nature"},poodle:{keywords:["dog","animal","101","nature","pet"],char:"🐩",fitzpatrick_scale:false,category:"animals_and_nature"},cat2:{keywords:["animal","meow","pet","cats"],char:"🐈",fitzpatrick_scale:false,category:"animals_and_nature"},rabbit2:{keywords:["animal","nature","pet","magic","spring"],char:"🐇",fitzpatrick_scale:false,category:"animals_and_nature"},chipmunk:{keywords:["animal","nature","rodent","squirrel"],char:"🐿",fitzpatrick_scale:false,category:"animals_and_nature"},hedgehog:{keywords:["animal","nature","spiny"],char:"🦔",fitzpatrick_scale:false,category:"animals_and_nature"},raccoon:{keywords:["animal","nature"],char:"🦝",fitzpatrick_scale:false,category:"animals_and_nature"},llama:{keywords:["animal","nature","alpaca"],char:"🦙",fitzpatrick_scale:false,category:"animals_and_nature"},hippopotamus:{keywords:["animal","nature"],char:"🦛",fitzpatrick_scale:false,category:"animals_and_nature"},kangaroo:{keywords:["animal","nature","australia","joey","hop","marsupial"],char:"🦘",fitzpatrick_scale:false,category:"animals_and_nature"},badger:{keywords:["animal","nature","honey"],char:"🦡",fitzpatrick_scale:false,category:"animals_and_nature"},swan:{keywords:["animal","nature","bird"],char:"🦢",fitzpatrick_scale:false,category:"animals_and_nature"},peacock:{keywords:["animal","nature","peahen","bird"],char:"🦚",fitzpatrick_scale:false,category:"animals_and_nature"},parrot:{keywords:["animal","nature","bird","pirate","talk"],char:"🦜",fitzpatrick_scale:false,category:"animals_and_nature"},lobster:{keywords:["animal","nature","bisque","claws","seafood"],char:"🦞",fitzpatrick_scale:false,category:"animals_and_nature"},mosquito:{keywords:["animal","nature","insect","malaria"],char:"🦟",fitzpatrick_scale:false,category:"animals_and_nature"},paw_prints:{keywords:["animal","tracking","footprints","dog","cat","pet","feet"],char:"🐾",fitzpatrick_scale:false,category:"animals_and_nature"},dragon:{keywords:["animal","myth","nature","chinese","green"],char:"🐉",fitzpatrick_scale:false,category:"animals_and_nature"},dragon_face:{keywords:["animal","myth","nature","chinese","green"],char:"🐲",fitzpatrick_scale:false,category:"animals_and_nature"},cactus:{keywords:["vegetable","plant","nature"],char:"🌵",fitzpatrick_scale:false,category:"animals_and_nature"},christmas_tree:{keywords:["festival","vacation","december","xmas","celebration"],char:"🎄",fitzpatrick_scale:false,category:"animals_and_nature"},evergreen_tree:{keywords:["plant","nature"],char:"🌲",fitzpatrick_scale:false,category:"animals_and_nature"},deciduous_tree:{keywords:["plant","nature"],char:"🌳",fitzpatrick_scale:false,category:"animals_and_nature"},palm_tree:{keywords:["plant","vegetable","nature","summer","beach","mojito","tropical"],char:"🌴",fitzpatrick_scale:false,category:"animals_and_nature"},seedling:{keywords:["plant","nature","grass","lawn","spring"],char:"🌱",fitzpatrick_scale:false,category:"animals_and_nature"},herb:{keywords:["vegetable","plant","medicine","weed","grass","lawn"],char:"🌿",fitzpatrick_scale:false,category:"animals_and_nature"},shamrock:{keywords:["vegetable","plant","nature","irish","clover"],char:"☘",fitzpatrick_scale:false,category:"animals_and_nature"},four_leaf_clover:{keywords:["vegetable","plant","nature","lucky","irish"],char:"🍀",fitzpatrick_scale:false,category:"animals_and_nature"},bamboo:{keywords:["plant","nature","vegetable","panda","pine_decoration"],char:"🎍",fitzpatrick_scale:false,category:"animals_and_nature"},tanabata_tree:{keywords:["plant","nature","branch","summer"],char:"🎋",fitzpatrick_scale:false,category:"animals_and_nature"},leaves:{keywords:["nature","plant","tree","vegetable","grass","lawn","spring"],char:"🍃",fitzpatrick_scale:false,category:"animals_and_nature"},fallen_leaf:{keywords:["nature","plant","vegetable","leaves"],char:"🍂",fitzpatrick_scale:false,category:"animals_and_nature"},maple_leaf:{keywords:["nature","plant","vegetable","ca","fall"],char:"🍁",fitzpatrick_scale:false,category:"animals_and_nature"},ear_of_rice:{keywords:["nature","plant"],char:"🌾",fitzpatrick_scale:false,category:"animals_and_nature"},hibiscus:{keywords:["plant","vegetable","flowers","beach"],char:"🌺",fitzpatrick_scale:false,category:"animals_and_nature"},sunflower:{keywords:["nature","plant","fall"],char:"🌻",fitzpatrick_scale:false,category:"animals_and_nature"},rose:{keywords:["flowers","valentines","love","spring"],char:"🌹",fitzpatrick_scale:false,category:"animals_and_nature"},wilted_flower:{keywords:["plant","nature","flower"],char:"🥀",fitzpatrick_scale:false,category:"animals_and_nature"},tulip:{keywords:["flowers","plant","nature","summer","spring"],char:"🌷",fitzpatrick_scale:false,category:"animals_and_nature"},blossom:{keywords:["nature","flowers","yellow"],char:"🌼",fitzpatrick_scale:false,category:"animals_and_nature"},cherry_blossom:{keywords:["nature","plant","spring","flower"],char:"🌸",fitzpatrick_scale:false,category:"animals_and_nature"},bouquet:{keywords:["flowers","nature","spring"],char:"💐",fitzpatrick_scale:false,category:"animals_and_nature"},mushroom:{keywords:["plant","vegetable"],char:"🍄",fitzpatrick_scale:false,category:"animals_and_nature"},chestnut:{keywords:["food","squirrel"],char:"🌰",fitzpatrick_scale:false,category:"animals_and_nature"},jack_o_lantern:{keywords:["halloween","light","pumpkin","creepy","fall"],char:"🎃",fitzpatrick_scale:false,category:"animals_and_nature"},shell:{keywords:["nature","sea","beach"],char:"🐚",fitzpatrick_scale:false,category:"animals_and_nature"},spider_web:{keywords:["animal","insect","arachnid","silk"],char:"🕸",fitzpatrick_scale:false,category:"animals_and_nature"},earth_americas:{keywords:["globe","world","USA","international"],char:"🌎",fitzpatrick_scale:false,category:"animals_and_nature"},earth_africa:{keywords:["globe","world","international"],char:"🌍",fitzpatrick_scale:false,category:"animals_and_nature"},earth_asia:{keywords:["globe","world","east","international"],char:"🌏",fitzpatrick_scale:false,category:"animals_and_nature"},full_moon:{keywords:["nature","yellow","twilight","planet","space","night","evening","sleep"],char:"🌕",fitzpatrick_scale:false,category:"animals_and_nature"},waning_gibbous_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"],char:"🌖",fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌗",fitzpatrick_scale:false,category:"animals_and_nature"},waning_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌘",fitzpatrick_scale:false,category:"animals_and_nature"},new_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌑",fitzpatrick_scale:false,category:"animals_and_nature"},waxing_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌒",fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌓",fitzpatrick_scale:false,category:"animals_and_nature"},waxing_gibbous_moon:{keywords:["nature","night","sky","gray","twilight","planet","space","evening","sleep"],char:"🌔",fitzpatrick_scale:false,category:"animals_and_nature"},new_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌚",fitzpatrick_scale:false,category:"animals_and_nature"},full_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌝",fitzpatrick_scale:false,category:"animals_and_nature"},first_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌛",fitzpatrick_scale:false,category:"animals_and_nature"},last_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"🌜",fitzpatrick_scale:false,category:"animals_and_nature"},sun_with_face:{keywords:["nature","morning","sky"],char:"🌞",fitzpatrick_scale:false,category:"animals_and_nature"},crescent_moon:{keywords:["night","sleep","sky","evening","magic"],char:"🌙",fitzpatrick_scale:false,category:"animals_and_nature"},star:{keywords:["night","yellow"],char:"⭐",fitzpatrick_scale:false,category:"animals_and_nature"},star2:{keywords:["night","sparkle","awesome","good","magic"],char:"🌟",fitzpatrick_scale:false,category:"animals_and_nature"},dizzy:{keywords:["star","sparkle","shoot","magic"],char:"💫",fitzpatrick_scale:false,category:"animals_and_nature"},sparkles:{keywords:["stars","shine","shiny","cool","awesome","good","magic"],char:"✨",fitzpatrick_scale:false,category:"animals_and_nature"},comet:{keywords:["space"],char:"☄",fitzpatrick_scale:false,category:"animals_and_nature"},sunny:{keywords:["weather","nature","brightness","summer","beach","spring"],char:"☀️",fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_small_cloud:{keywords:["weather"],char:"🌤",fitzpatrick_scale:false,category:"animals_and_nature"},partly_sunny:{keywords:["weather","nature","cloudy","morning","fall","spring"],char:"⛅",fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_large_cloud:{keywords:["weather"],char:"🌥",fitzpatrick_scale:false,category:"animals_and_nature"},sun_behind_rain_cloud:{keywords:["weather"],char:"🌦",fitzpatrick_scale:false,category:"animals_and_nature"},cloud:{keywords:["weather","sky"],char:"☁️",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_rain:{keywords:["weather"],char:"🌧",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning_and_rain:{keywords:["weather","lightning"],char:"⛈",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_lightning:{keywords:["weather","thunder"],char:"🌩",fitzpatrick_scale:false,category:"animals_and_nature"},zap:{keywords:["thunder","weather","lightning bolt","fast"],char:"⚡",fitzpatrick_scale:false,category:"animals_and_nature"},fire:{keywords:["hot","cook","flame"],char:"🔥",fitzpatrick_scale:false,category:"animals_and_nature"},boom:{keywords:["bomb","explode","explosion","collision","blown"],char:"💥",fitzpatrick_scale:false,category:"animals_and_nature"},snowflake:{keywords:["winter","season","cold","weather","christmas","xmas"],char:"❄️",fitzpatrick_scale:false,category:"animals_and_nature"},cloud_with_snow:{keywords:["weather"],char:"🌨",fitzpatrick_scale:false,category:"animals_and_nature"},snowman:{keywords:["winter","season","cold","weather","christmas","xmas","frozen","without_snow"],char:"⛄",fitzpatrick_scale:false,category:"animals_and_nature"},snowman_with_snow:{keywords:["winter","season","cold","weather","christmas","xmas","frozen"],char:"☃",fitzpatrick_scale:false,category:"animals_and_nature"},wind_face:{keywords:["gust","air"],char:"🌬",fitzpatrick_scale:false,category:"animals_and_nature"},dash:{keywords:["wind","air","fast","shoo","fart","smoke","puff"],char:"💨",fitzpatrick_scale:false,category:"animals_and_nature"},tornado:{keywords:["weather","cyclone","twister"],char:"🌪",fitzpatrick_scale:false,category:"animals_and_nature"},fog:{keywords:["weather"],char:"🌫",fitzpatrick_scale:false,category:"animals_and_nature"},open_umbrella:{keywords:["weather","spring"],char:"☂",fitzpatrick_scale:false,category:"animals_and_nature"},umbrella:{keywords:["rainy","weather","spring"],char:"☔",fitzpatrick_scale:false,category:"animals_and_nature"},droplet:{keywords:["water","drip","faucet","spring"],char:"💧",fitzpatrick_scale:false,category:"animals_and_nature"},sweat_drops:{keywords:["water","drip","oops"],char:"💦",fitzpatrick_scale:false,category:"animals_and_nature"},ocean:{keywords:["sea","water","wave","nature","tsunami","disaster"],char:"🌊",fitzpatrick_scale:false,category:"animals_and_nature"},green_apple:{keywords:["fruit","nature"],char:"🍏",fitzpatrick_scale:false,category:"food_and_drink"},apple:{keywords:["fruit","mac","school"],char:"🍎",fitzpatrick_scale:false,category:"food_and_drink"},pear:{keywords:["fruit","nature","food"],char:"🍐",fitzpatrick_scale:false,category:"food_and_drink"},tangerine:{keywords:["food","fruit","nature","orange"],char:"🍊",fitzpatrick_scale:false,category:"food_and_drink"},lemon:{keywords:["fruit","nature"],char:"🍋",fitzpatrick_scale:false,category:"food_and_drink"},banana:{keywords:["fruit","food","monkey"],char:"🍌",fitzpatrick_scale:false,category:"food_and_drink"},watermelon:{keywords:["fruit","food","picnic","summer"],char:"🍉",fitzpatrick_scale:false,category:"food_and_drink"},grapes:{keywords:["fruit","food","wine"],char:"🍇",fitzpatrick_scale:false,category:"food_and_drink"},strawberry:{keywords:["fruit","food","nature"],char:"🍓",fitzpatrick_scale:false,category:"food_and_drink"},melon:{keywords:["fruit","nature","food"],char:"🍈",fitzpatrick_scale:false,category:"food_and_drink"},cherries:{keywords:["food","fruit"],char:"🍒",fitzpatrick_scale:false,category:"food_and_drink"},peach:{keywords:["fruit","nature","food"],char:"🍑",fitzpatrick_scale:false,category:"food_and_drink"},pineapple:{keywords:["fruit","nature","food"],char:"🍍",fitzpatrick_scale:false,category:"food_and_drink"},coconut:{keywords:["fruit","nature","food","palm"],char:"🥥",fitzpatrick_scale:false,category:"food_and_drink"},kiwi_fruit:{keywords:["fruit","food"],char:"🥝",fitzpatrick_scale:false,category:"food_and_drink"},mango:{keywords:["fruit","food","tropical"],char:"🥭",fitzpatrick_scale:false,category:"food_and_drink"},avocado:{keywords:["fruit","food"],char:"🥑",fitzpatrick_scale:false,category:"food_and_drink"},broccoli:{keywords:["fruit","food","vegetable"],char:"🥦",fitzpatrick_scale:false,category:"food_and_drink"},tomato:{keywords:["fruit","vegetable","nature","food"],char:"🍅",fitzpatrick_scale:false,category:"food_and_drink"},eggplant:{keywords:["vegetable","nature","food","aubergine"],char:"🍆",fitzpatrick_scale:false,category:"food_and_drink"},cucumber:{keywords:["fruit","food","pickle"],char:"🥒",fitzpatrick_scale:false,category:"food_and_drink"},carrot:{keywords:["vegetable","food","orange"],char:"🥕",fitzpatrick_scale:false,category:"food_and_drink"},hot_pepper:{keywords:["food","spicy","chilli","chili"],char:"🌶",fitzpatrick_scale:false,category:"food_and_drink"},potato:{keywords:["food","tuber","vegatable","starch"],char:"🥔",fitzpatrick_scale:false,category:"food_and_drink"},corn:{keywords:["food","vegetable","plant"],char:"🌽",fitzpatrick_scale:false,category:"food_and_drink"},leafy_greens:{keywords:["food","vegetable","plant","bok choy","cabbage","kale","lettuce"],char:"🥬",fitzpatrick_scale:false,category:"food_and_drink"},sweet_potato:{keywords:["food","nature"],char:"🍠",fitzpatrick_scale:false,category:"food_and_drink"},peanuts:{keywords:["food","nut"],char:"🥜",fitzpatrick_scale:false,category:"food_and_drink"},honey_pot:{keywords:["bees","sweet","kitchen"],char:"🍯",fitzpatrick_scale:false,category:"food_and_drink"},croissant:{keywords:["food","bread","french"],char:"🥐",fitzpatrick_scale:false,category:"food_and_drink"},bread:{keywords:["food","wheat","breakfast","toast"],char:"🍞",fitzpatrick_scale:false,category:"food_and_drink"},baguette_bread:{keywords:["food","bread","french"],char:"🥖",fitzpatrick_scale:false,category:"food_and_drink"},bagel:{keywords:["food","bread","bakery","schmear"],char:"🥯",fitzpatrick_scale:false,category:"food_and_drink"},pretzel:{keywords:["food","bread","twisted"],char:"🥨",fitzpatrick_scale:false,category:"food_and_drink"},cheese:{keywords:["food","chadder"],char:"🧀",fitzpatrick_scale:false,category:"food_and_drink"},egg:{keywords:["food","chicken","breakfast"],char:"🥚",fitzpatrick_scale:false,category:"food_and_drink"},bacon:{keywords:["food","breakfast","pork","pig","meat"],char:"🥓",fitzpatrick_scale:false,category:"food_and_drink"},steak:{keywords:["food","cow","meat","cut","chop","lambchop","porkchop"],char:"🥩",fitzpatrick_scale:false,category:"food_and_drink"},pancakes:{keywords:["food","breakfast","flapjacks","hotcakes"],char:"🥞",fitzpatrick_scale:false,category:"food_and_drink"},poultry_leg:{keywords:["food","meat","drumstick","bird","chicken","turkey"],char:"🍗",fitzpatrick_scale:false,category:"food_and_drink"},meat_on_bone:{keywords:["good","food","drumstick"],char:"🍖",fitzpatrick_scale:false,category:"food_and_drink"},bone:{keywords:["skeleton"],char:"🦴",fitzpatrick_scale:false,category:"food_and_drink"},fried_shrimp:{keywords:["food","animal","appetizer","summer"],char:"🍤",fitzpatrick_scale:false,category:"food_and_drink"},fried_egg:{keywords:["food","breakfast","kitchen","egg"],char:"🍳",fitzpatrick_scale:false,category:"food_and_drink"},hamburger:{keywords:["meat","fast food","beef","cheeseburger","mcdonalds","burger king"],char:"🍔",fitzpatrick_scale:false,category:"food_and_drink"},fries:{keywords:["chips","snack","fast food"],char:"🍟",fitzpatrick_scale:false,category:"food_and_drink"},stuffed_flatbread:{keywords:["food","flatbread","stuffed","gyro"],char:"🥙",fitzpatrick_scale:false,category:"food_and_drink"},hotdog:{keywords:["food","frankfurter"],char:"🌭",fitzpatrick_scale:false,category:"food_and_drink"},pizza:{keywords:["food","party"],char:"🍕",fitzpatrick_scale:false,category:"food_and_drink"},sandwich:{keywords:["food","lunch","bread"],char:"🥪",fitzpatrick_scale:false,category:"food_and_drink"},canned_food:{keywords:["food","soup"],char:"🥫",fitzpatrick_scale:false,category:"food_and_drink"},spaghetti:{keywords:["food","italian","noodle"],char:"🍝",fitzpatrick_scale:false,category:"food_and_drink"},taco:{keywords:["food","mexican"],char:"🌮",fitzpatrick_scale:false,category:"food_and_drink"},burrito:{keywords:["food","mexican"],char:"🌯",fitzpatrick_scale:false,category:"food_and_drink"},green_salad:{keywords:["food","healthy","lettuce"],char:"🥗",fitzpatrick_scale:false,category:"food_and_drink"},shallow_pan_of_food:{keywords:["food","cooking","casserole","paella"],char:"🥘",fitzpatrick_scale:false,category:"food_and_drink"},ramen:{keywords:["food","japanese","noodle","chopsticks"],char:"🍜",fitzpatrick_scale:false,category:"food_and_drink"},stew:{keywords:["food","meat","soup"],char:"🍲",fitzpatrick_scale:false,category:"food_and_drink"},fish_cake:{keywords:["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"],char:"🍥",fitzpatrick_scale:false,category:"food_and_drink"},fortune_cookie:{keywords:["food","prophecy"],char:"🥠",fitzpatrick_scale:false,category:"food_and_drink"},sushi:{keywords:["food","fish","japanese","rice"],char:"🍣",fitzpatrick_scale:false,category:"food_and_drink"},bento:{keywords:["food","japanese","box"],char:"🍱",fitzpatrick_scale:false,category:"food_and_drink"},curry:{keywords:["food","spicy","hot","indian"],char:"🍛",fitzpatrick_scale:false,category:"food_and_drink"},rice_ball:{keywords:["food","japanese"],char:"🍙",fitzpatrick_scale:false,category:"food_and_drink"},rice:{keywords:["food","china","asian"],char:"🍚",fitzpatrick_scale:false,category:"food_and_drink"},rice_cracker:{keywords:["food","japanese"],char:"🍘",fitzpatrick_scale:false,category:"food_and_drink"},oden:{keywords:["food","japanese"],char:"🍢",fitzpatrick_scale:false,category:"food_and_drink"},dango:{keywords:["food","dessert","sweet","japanese","barbecue","meat"],char:"🍡",fitzpatrick_scale:false,category:"food_and_drink"},shaved_ice:{keywords:["hot","dessert","summer"],char:"🍧",fitzpatrick_scale:false,category:"food_and_drink"},ice_cream:{keywords:["food","hot","dessert"],char:"🍨",fitzpatrick_scale:false,category:"food_and_drink"},icecream:{keywords:["food","hot","dessert","summer"],char:"🍦",fitzpatrick_scale:false,category:"food_and_drink"},pie:{keywords:["food","dessert","pastry"],char:"🥧",fitzpatrick_scale:false,category:"food_and_drink"},cake:{keywords:["food","dessert"],char:"🍰",fitzpatrick_scale:false,category:"food_and_drink"},cupcake:{keywords:["food","dessert","bakery","sweet"],char:"🧁",fitzpatrick_scale:false,category:"food_and_drink"},moon_cake:{keywords:["food","autumn"],char:"🥮",fitzpatrick_scale:false,category:"food_and_drink"},birthday:{keywords:["food","dessert","cake"],char:"🎂",fitzpatrick_scale:false,category:"food_and_drink"},custard:{keywords:["dessert","food"],char:"🍮",fitzpatrick_scale:false,category:"food_and_drink"},candy:{keywords:["snack","dessert","sweet","lolly"],char:"🍬",fitzpatrick_scale:false,category:"food_and_drink"},lollipop:{keywords:["food","snack","candy","sweet"],char:"🍭",fitzpatrick_scale:false,category:"food_and_drink"},chocolate_bar:{keywords:["food","snack","dessert","sweet"],char:"🍫",fitzpatrick_scale:false,category:"food_and_drink"},popcorn:{keywords:["food","movie theater","films","snack"],char:"🍿",fitzpatrick_scale:false,category:"food_and_drink"},dumpling:{keywords:["food","empanada","pierogi","potsticker"],char:"🥟",fitzpatrick_scale:false,category:"food_and_drink"},doughnut:{keywords:["food","dessert","snack","sweet","donut"],char:"🍩",fitzpatrick_scale:false,category:"food_and_drink"},cookie:{keywords:["food","snack","oreo","chocolate","sweet","dessert"],char:"🍪",fitzpatrick_scale:false,category:"food_and_drink"},milk_glass:{keywords:["beverage","drink","cow"],char:"🥛",fitzpatrick_scale:false,category:"food_and_drink"},beer:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:"🍺",fitzpatrick_scale:false,category:"food_and_drink"},beers:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:"🍻",fitzpatrick_scale:false,category:"food_and_drink"},clinking_glasses:{keywords:["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"],char:"🥂",fitzpatrick_scale:false,category:"food_and_drink"},wine_glass:{keywords:["drink","beverage","drunk","alcohol","booze"],char:"🍷",fitzpatrick_scale:false,category:"food_and_drink"},tumbler_glass:{keywords:["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"],char:"🥃",fitzpatrick_scale:false,category:"food_and_drink"},cocktail:{keywords:["drink","drunk","alcohol","beverage","booze","mojito"],char:"🍸",fitzpatrick_scale:false,category:"food_and_drink"},tropical_drink:{keywords:["beverage","cocktail","summer","beach","alcohol","booze","mojito"],char:"🍹",fitzpatrick_scale:false,category:"food_and_drink"},champagne:{keywords:["drink","wine","bottle","celebration"],char:"🍾",fitzpatrick_scale:false,category:"food_and_drink"},sake:{keywords:["wine","drink","drunk","beverage","japanese","alcohol","booze"],char:"🍶",fitzpatrick_scale:false,category:"food_and_drink"},tea:{keywords:["drink","bowl","breakfast","green","british"],char:"🍵",fitzpatrick_scale:false,category:"food_and_drink"},cup_with_straw:{keywords:["drink","soda"],char:"🥤",fitzpatrick_scale:false,category:"food_and_drink"},coffee:{keywords:["beverage","caffeine","latte","espresso"],char:"☕",fitzpatrick_scale:false,category:"food_and_drink"},baby_bottle:{keywords:["food","container","milk"],char:"🍼",fitzpatrick_scale:false,category:"food_and_drink"},salt:{keywords:["condiment","shaker"],char:"🧂",fitzpatrick_scale:false,category:"food_and_drink"},spoon:{keywords:["cutlery","kitchen","tableware"],char:"🥄",fitzpatrick_scale:false,category:"food_and_drink"},fork_and_knife:{keywords:["cutlery","kitchen"],char:"🍴",fitzpatrick_scale:false,category:"food_and_drink"},plate_with_cutlery:{keywords:["food","eat","meal","lunch","dinner","restaurant"],char:"🍽",fitzpatrick_scale:false,category:"food_and_drink"},bowl_with_spoon:{keywords:["food","breakfast","cereal","oatmeal","porridge"],char:"🥣",fitzpatrick_scale:false,category:"food_and_drink"},takeout_box:{keywords:["food","leftovers"],char:"🥡",fitzpatrick_scale:false,category:"food_and_drink"},chopsticks:{keywords:["food"],char:"🥢",fitzpatrick_scale:false,category:"food_and_drink"},soccer:{keywords:["sports","football"],char:"⚽",fitzpatrick_scale:false,category:"activity"},basketball:{keywords:["sports","balls","NBA"],char:"🏀",fitzpatrick_scale:false,category:"activity"},football:{keywords:["sports","balls","NFL"],char:"🏈",fitzpatrick_scale:false,category:"activity"},baseball:{keywords:["sports","balls"],char:"⚾",fitzpatrick_scale:false,category:"activity"},softball:{keywords:["sports","balls"],char:"🥎",fitzpatrick_scale:false,category:"activity"},tennis:{keywords:["sports","balls","green"],char:"🎾",fitzpatrick_scale:false,category:"activity"},volleyball:{keywords:["sports","balls"],char:"🏐",fitzpatrick_scale:false,category:"activity"},rugby_football:{keywords:["sports","team"],char:"🏉",fitzpatrick_scale:false,category:"activity"},flying_disc:{keywords:["sports","frisbee","ultimate"],char:"🥏",fitzpatrick_scale:false,category:"activity"},"8ball":{keywords:["pool","hobby","game","luck","magic"],char:"🎱",fitzpatrick_scale:false,category:"activity"},golf:{keywords:["sports","business","flag","hole","summer"],char:"⛳",fitzpatrick_scale:false,category:"activity"},golfing_woman:{keywords:["sports","business","woman","female"],char:"🏌️‍♀️",fitzpatrick_scale:false,category:"activity"},golfing_man:{keywords:["sports","business"],char:"🏌",fitzpatrick_scale:true,category:"activity"},ping_pong:{keywords:["sports","pingpong"],char:"🏓",fitzpatrick_scale:false,category:"activity"},badminton:{keywords:["sports"],char:"🏸",fitzpatrick_scale:false,category:"activity"},goal_net:{keywords:["sports"],char:"🥅",fitzpatrick_scale:false,category:"activity"},ice_hockey:{keywords:["sports"],char:"🏒",fitzpatrick_scale:false,category:"activity"},field_hockey:{keywords:["sports"],char:"🏑",fitzpatrick_scale:false,category:"activity"},lacrosse:{keywords:["sports","ball","stick"],char:"🥍",fitzpatrick_scale:false,category:"activity"},cricket:{keywords:["sports"],char:"🏏",fitzpatrick_scale:false,category:"activity"},ski:{keywords:["sports","winter","cold","snow"],char:"🎿",fitzpatrick_scale:false,category:"activity"},skier:{keywords:["sports","winter","snow"],char:"⛷",fitzpatrick_scale:false,category:"activity"},snowboarder:{keywords:["sports","winter"],char:"🏂",fitzpatrick_scale:true,category:"activity"},person_fencing:{keywords:["sports","fencing","sword"],char:"🤺",fitzpatrick_scale:false,category:"activity"},women_wrestling:{keywords:["sports","wrestlers"],char:"🤼‍♀️",fitzpatrick_scale:false,category:"activity"},men_wrestling:{keywords:["sports","wrestlers"],char:"🤼‍♂️",fitzpatrick_scale:false,category:"activity"},woman_cartwheeling:{keywords:["gymnastics"],char:"🤸‍♀️",fitzpatrick_scale:true,category:"activity"},man_cartwheeling:{keywords:["gymnastics"],char:"🤸‍♂️",fitzpatrick_scale:true,category:"activity"},woman_playing_handball:{keywords:["sports"],char:"🤾‍♀️",fitzpatrick_scale:true,category:"activity"},man_playing_handball:{keywords:["sports"],char:"🤾‍♂️",fitzpatrick_scale:true,category:"activity"},ice_skate:{keywords:["sports"],char:"⛸",fitzpatrick_scale:false,category:"activity"},curling_stone:{keywords:["sports"],char:"🥌",fitzpatrick_scale:false,category:"activity"},skateboard:{keywords:["board"],char:"🛹",fitzpatrick_scale:false,category:"activity"},sled:{keywords:["sleigh","luge","toboggan"],char:"🛷",fitzpatrick_scale:false,category:"activity"},bow_and_arrow:{keywords:["sports"],char:"🏹",fitzpatrick_scale:false,category:"activity"},fishing_pole_and_fish:{keywords:["food","hobby","summer"],char:"🎣",fitzpatrick_scale:false,category:"activity"},boxing_glove:{keywords:["sports","fighting"],char:"🥊",fitzpatrick_scale:false,category:"activity"},martial_arts_uniform:{keywords:["judo","karate","taekwondo"],char:"🥋",fitzpatrick_scale:false,category:"activity"},rowing_woman:{keywords:["sports","hobby","water","ship","woman","female"],char:"🚣‍♀️",fitzpatrick_scale:true,category:"activity"},rowing_man:{keywords:["sports","hobby","water","ship"],char:"🚣",fitzpatrick_scale:true,category:"activity"},climbing_woman:{keywords:["sports","hobby","woman","female","rock"],char:"🧗‍♀️",fitzpatrick_scale:true,category:"activity"},climbing_man:{keywords:["sports","hobby","man","male","rock"],char:"🧗‍♂️",fitzpatrick_scale:true,category:"activity"},swimming_woman:{keywords:["sports","exercise","human","athlete","water","summer","woman","female"],char:"🏊‍♀️",fitzpatrick_scale:true,category:"activity"},swimming_man:{keywords:["sports","exercise","human","athlete","water","summer"],char:"🏊",fitzpatrick_scale:true,category:"activity"},woman_playing_water_polo:{keywords:["sports","pool"],char:"🤽‍♀️",fitzpatrick_scale:true,category:"activity"},man_playing_water_polo:{keywords:["sports","pool"],char:"🤽‍♂️",fitzpatrick_scale:true,category:"activity"},woman_in_lotus_position:{keywords:["woman","female","meditation","yoga","serenity","zen","mindfulness"],char:"🧘‍♀️",fitzpatrick_scale:true,category:"activity"},man_in_lotus_position:{keywords:["man","male","meditation","yoga","serenity","zen","mindfulness"],char:"🧘‍♂️",fitzpatrick_scale:true,category:"activity"},surfing_woman:{keywords:["sports","ocean","sea","summer","beach","woman","female"],char:"🏄‍♀️",fitzpatrick_scale:true,category:"activity"},surfing_man:{keywords:["sports","ocean","sea","summer","beach"],char:"🏄",fitzpatrick_scale:true,category:"activity"},bath:{keywords:["clean","shower","bathroom"],char:"🛀",fitzpatrick_scale:true,category:"activity"},basketball_woman:{keywords:["sports","human","woman","female"],char:"⛹️‍♀️",fitzpatrick_scale:true,category:"activity"},basketball_man:{keywords:["sports","human"],char:"⛹",fitzpatrick_scale:true,category:"activity"},weight_lifting_woman:{keywords:["sports","training","exercise","woman","female"],char:"🏋️‍♀️",fitzpatrick_scale:true,category:"activity"},weight_lifting_man:{keywords:["sports","training","exercise"],char:"🏋",fitzpatrick_scale:true,category:"activity"},biking_woman:{keywords:["sports","bike","exercise","hipster","woman","female"],char:"🚴‍♀️",fitzpatrick_scale:true,category:"activity"},biking_man:{keywords:["sports","bike","exercise","hipster"],char:"🚴",fitzpatrick_scale:true,category:"activity"},mountain_biking_woman:{keywords:["transportation","sports","human","race","bike","woman","female"],char:"🚵‍♀️",fitzpatrick_scale:true,category:"activity"},mountain_biking_man:{keywords:["transportation","sports","human","race","bike"],char:"🚵",fitzpatrick_scale:true,category:"activity"},horse_racing:{keywords:["animal","betting","competition","gambling","luck"],char:"🏇",fitzpatrick_scale:true,category:"activity"},business_suit_levitating:{keywords:["suit","business","levitate","hover","jump"],char:"🕴",fitzpatrick_scale:true,category:"activity"},trophy:{keywords:["win","award","contest","place","ftw","ceremony"],char:"🏆",fitzpatrick_scale:false,category:"activity"},running_shirt_with_sash:{keywords:["play","pageant"],char:"🎽",fitzpatrick_scale:false,category:"activity"},medal_sports:{keywords:["award","winning"],char:"🏅",fitzpatrick_scale:false,category:"activity"},medal_military:{keywords:["award","winning","army"],char:"🎖",fitzpatrick_scale:false,category:"activity"},"1st_place_medal":{keywords:["award","winning","first"],char:"🥇",fitzpatrick_scale:false,category:"activity"},"2nd_place_medal":{keywords:["award","second"],char:"🥈",fitzpatrick_scale:false,category:"activity"},"3rd_place_medal":{keywords:["award","third"],char:"🥉",fitzpatrick_scale:false,category:"activity"},reminder_ribbon:{keywords:["sports","cause","support","awareness"],char:"🎗",fitzpatrick_scale:false,category:"activity"},rosette:{keywords:["flower","decoration","military"],char:"🏵",fitzpatrick_scale:false,category:"activity"},ticket:{keywords:["event","concert","pass"],char:"🎫",fitzpatrick_scale:false,category:"activity"},tickets:{keywords:["sports","concert","entrance"],char:"🎟",fitzpatrick_scale:false,category:"activity"},performing_arts:{keywords:["acting","theater","drama"],char:"🎭",fitzpatrick_scale:false,category:"activity"},art:{keywords:["design","paint","draw","colors"],char:"🎨",fitzpatrick_scale:false,category:"activity"},circus_tent:{keywords:["festival","carnival","party"],char:"🎪",fitzpatrick_scale:false,category:"activity"},woman_juggling:{keywords:["juggle","balance","skill","multitask"],char:"🤹‍♀️",fitzpatrick_scale:true,category:"activity"},man_juggling:{keywords:["juggle","balance","skill","multitask"],char:"🤹‍♂️",fitzpatrick_scale:true,category:"activity"},microphone:{keywords:["sound","music","PA","sing","talkshow"],char:"🎤",fitzpatrick_scale:false,category:"activity"},headphones:{keywords:["music","score","gadgets"],char:"🎧",fitzpatrick_scale:false,category:"activity"},musical_score:{keywords:["treble","clef","compose"],char:"🎼",fitzpatrick_scale:false,category:"activity"},musical_keyboard:{keywords:["piano","instrument","compose"],char:"🎹",fitzpatrick_scale:false,category:"activity"},drum:{keywords:["music","instrument","drumsticks","snare"],char:"🥁",fitzpatrick_scale:false,category:"activity"},saxophone:{keywords:["music","instrument","jazz","blues"],char:"🎷",fitzpatrick_scale:false,category:"activity"},trumpet:{keywords:["music","brass"],char:"🎺",fitzpatrick_scale:false,category:"activity"},guitar:{keywords:["music","instrument"],char:"🎸",fitzpatrick_scale:false,category:"activity"},violin:{keywords:["music","instrument","orchestra","symphony"],char:"🎻",fitzpatrick_scale:false,category:"activity"},clapper:{keywords:["movie","film","record"],char:"🎬",fitzpatrick_scale:false,category:"activity"},video_game:{keywords:["play","console","PS4","controller"],char:"🎮",fitzpatrick_scale:false,category:"activity"},space_invader:{keywords:["game","arcade","play"],char:"👾",fitzpatrick_scale:false,category:"activity"},dart:{keywords:["game","play","bar","target","bullseye"],char:"🎯",fitzpatrick_scale:false,category:"activity"},game_die:{keywords:["dice","random","tabletop","play","luck"],char:"🎲",fitzpatrick_scale:false,category:"activity"},chess_pawn:{keywords:["expendable"],char:"♟",fitzpatrick_scale:false,category:"activity"},slot_machine:{keywords:["bet","gamble","vegas","fruit machine","luck","casino"],char:"🎰",fitzpatrick_scale:false,category:"activity"},jigsaw:{keywords:["interlocking","puzzle","piece"],char:"🧩",fitzpatrick_scale:false,category:"activity"},bowling:{keywords:["sports","fun","play"],char:"🎳",fitzpatrick_scale:false,category:"activity"},red_car:{keywords:["red","transportation","vehicle"],char:"🚗",fitzpatrick_scale:false,category:"travel_and_places"},taxi:{keywords:["uber","vehicle","cars","transportation"],char:"🚕",fitzpatrick_scale:false,category:"travel_and_places"},blue_car:{keywords:["transportation","vehicle"],char:"🚙",fitzpatrick_scale:false,category:"travel_and_places"},bus:{keywords:["car","vehicle","transportation"],char:"🚌",fitzpatrick_scale:false,category:"travel_and_places"},trolleybus:{keywords:["bart","transportation","vehicle"],char:"🚎",fitzpatrick_scale:false,category:"travel_and_places"},racing_car:{keywords:["sports","race","fast","formula","f1"],char:"🏎",fitzpatrick_scale:false,category:"travel_and_places"},police_car:{keywords:["vehicle","cars","transportation","law","legal","enforcement"],char:"🚓",fitzpatrick_scale:false,category:"travel_and_places"},ambulance:{keywords:["health","911","hospital"],char:"🚑",fitzpatrick_scale:false,category:"travel_and_places"},fire_engine:{keywords:["transportation","cars","vehicle"],char:"🚒",fitzpatrick_scale:false,category:"travel_and_places"},minibus:{keywords:["vehicle","car","transportation"],char:"🚐",fitzpatrick_scale:false,category:"travel_and_places"},truck:{keywords:["cars","transportation"],char:"🚚",fitzpatrick_scale:false,category:"travel_and_places"},articulated_lorry:{keywords:["vehicle","cars","transportation","express"],char:"🚛",fitzpatrick_scale:false,category:"travel_and_places"},tractor:{keywords:["vehicle","car","farming","agriculture"],char:"🚜",fitzpatrick_scale:false,category:"travel_and_places"},kick_scooter:{keywords:["vehicle","kick","razor"],char:"🛴",fitzpatrick_scale:false,category:"travel_and_places"},motorcycle:{keywords:["race","sports","fast"],char:"🏍",fitzpatrick_scale:false,category:"travel_and_places"},bike:{keywords:["sports","bicycle","exercise","hipster"],char:"🚲",fitzpatrick_scale:false,category:"travel_and_places"},motor_scooter:{keywords:["vehicle","vespa","sasha"],char:"🛵",fitzpatrick_scale:false,category:"travel_and_places"},rotating_light:{keywords:["police","ambulance","911","emergency","alert","error","pinged","law","legal"],char:"🚨",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_police_car:{keywords:["vehicle","law","legal","enforcement","911"],char:"🚔",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_bus:{keywords:["vehicle","transportation"],char:"🚍",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_automobile:{keywords:["car","vehicle","transportation"],char:"🚘",fitzpatrick_scale:false,category:"travel_and_places"},oncoming_taxi:{keywords:["vehicle","cars","uber"],char:"🚖",fitzpatrick_scale:false,category:"travel_and_places"},aerial_tramway:{keywords:["transportation","vehicle","ski"],char:"🚡",fitzpatrick_scale:false,category:"travel_and_places"},mountain_cableway:{keywords:["transportation","vehicle","ski"],char:"🚠",fitzpatrick_scale:false,category:"travel_and_places"},suspension_railway:{keywords:["vehicle","transportation"],char:"🚟",fitzpatrick_scale:false,category:"travel_and_places"},railway_car:{keywords:["transportation","vehicle"],char:"🚃",fitzpatrick_scale:false,category:"travel_and_places"},train:{keywords:["transportation","vehicle","carriage","public","travel"],char:"🚋",fitzpatrick_scale:false,category:"travel_and_places"},monorail:{keywords:["transportation","vehicle"],char:"🚝",fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_side:{keywords:["transportation","vehicle"],char:"🚄",fitzpatrick_scale:false,category:"travel_and_places"},bullettrain_front:{keywords:["transportation","vehicle","speed","fast","public","travel"],char:"🚅",fitzpatrick_scale:false,category:"travel_and_places"},light_rail:{keywords:["transportation","vehicle"],char:"🚈",fitzpatrick_scale:false,category:"travel_and_places"},mountain_railway:{keywords:["transportation","vehicle"],char:"🚞",fitzpatrick_scale:false,category:"travel_and_places"},steam_locomotive:{keywords:["transportation","vehicle","train"],char:"🚂",fitzpatrick_scale:false,category:"travel_and_places"},train2:{keywords:["transportation","vehicle"],char:"🚆",fitzpatrick_scale:false,category:"travel_and_places"},metro:{keywords:["transportation","blue-square","mrt","underground","tube"],char:"🚇",fitzpatrick_scale:false,category:"travel_and_places"},tram:{keywords:["transportation","vehicle"],char:"🚊",fitzpatrick_scale:false,category:"travel_and_places"},station:{keywords:["transportation","vehicle","public"],char:"🚉",fitzpatrick_scale:false,category:"travel_and_places"},flying_saucer:{keywords:["transportation","vehicle","ufo"],char:"🛸",fitzpatrick_scale:false,category:"travel_and_places"},helicopter:{keywords:["transportation","vehicle","fly"],char:"🚁",fitzpatrick_scale:false,category:"travel_and_places"},small_airplane:{keywords:["flight","transportation","fly","vehicle"],char:"🛩",fitzpatrick_scale:false,category:"travel_and_places"},airplane:{keywords:["vehicle","transportation","flight","fly"],char:"✈️",fitzpatrick_scale:false,category:"travel_and_places"},flight_departure:{keywords:["airport","flight","landing"],char:"🛫",fitzpatrick_scale:false,category:"travel_and_places"},flight_arrival:{keywords:["airport","flight","boarding"],char:"🛬",fitzpatrick_scale:false,category:"travel_and_places"},sailboat:{keywords:["ship","summer","transportation","water","sailing"],char:"⛵",fitzpatrick_scale:false,category:"travel_and_places"},motor_boat:{keywords:["ship"],char:"🛥",fitzpatrick_scale:false,category:"travel_and_places"},speedboat:{keywords:["ship","transportation","vehicle","summer"],char:"🚤",fitzpatrick_scale:false,category:"travel_and_places"},ferry:{keywords:["boat","ship","yacht"],char:"⛴",fitzpatrick_scale:false,category:"travel_and_places"},passenger_ship:{keywords:["yacht","cruise","ferry"],char:"🛳",fitzpatrick_scale:false,category:"travel_and_places"},rocket:{keywords:["launch","ship","staffmode","NASA","outer space","outer_space","fly"],char:"🚀",fitzpatrick_scale:false,category:"travel_and_places"},artificial_satellite:{keywords:["communication","gps","orbit","spaceflight","NASA","ISS"],char:"🛰",fitzpatrick_scale:false,category:"travel_and_places"},seat:{keywords:["sit","airplane","transport","bus","flight","fly"],char:"💺",fitzpatrick_scale:false,category:"travel_and_places"},canoe:{keywords:["boat","paddle","water","ship"],char:"🛶",fitzpatrick_scale:false,category:"travel_and_places"},anchor:{keywords:["ship","ferry","sea","boat"],char:"⚓",fitzpatrick_scale:false,category:"travel_and_places"},construction:{keywords:["wip","progress","caution","warning"],char:"🚧",fitzpatrick_scale:false,category:"travel_and_places"},fuelpump:{keywords:["gas station","petroleum"],char:"⛽",fitzpatrick_scale:false,category:"travel_and_places"},busstop:{keywords:["transportation","wait"],char:"🚏",fitzpatrick_scale:false,category:"travel_and_places"},vertical_traffic_light:{keywords:["transportation","driving"],char:"🚦",fitzpatrick_scale:false,category:"travel_and_places"},traffic_light:{keywords:["transportation","signal"],char:"🚥",fitzpatrick_scale:false,category:"travel_and_places"},checkered_flag:{keywords:["contest","finishline","race","gokart"],char:"🏁",fitzpatrick_scale:false,category:"travel_and_places"},ship:{keywords:["transportation","titanic","deploy"],char:"🚢",fitzpatrick_scale:false,category:"travel_and_places"},ferris_wheel:{keywords:["photo","carnival","londoneye"],char:"🎡",fitzpatrick_scale:false,category:"travel_and_places"},roller_coaster:{keywords:["carnival","playground","photo","fun"],char:"🎢",fitzpatrick_scale:false,category:"travel_and_places"},carousel_horse:{keywords:["photo","carnival"],char:"🎠",fitzpatrick_scale:false,category:"travel_and_places"},building_construction:{keywords:["wip","working","progress"],char:"🏗",fitzpatrick_scale:false,category:"travel_and_places"},foggy:{keywords:["photo","mountain"],char:"🌁",fitzpatrick_scale:false,category:"travel_and_places"},tokyo_tower:{keywords:["photo","japanese"],char:"🗼",fitzpatrick_scale:false,category:"travel_and_places"},factory:{keywords:["building","industry","pollution","smoke"],char:"🏭",fitzpatrick_scale:false,category:"travel_and_places"},fountain:{keywords:["photo","summer","water","fresh"],char:"⛲",fitzpatrick_scale:false,category:"travel_and_places"},rice_scene:{keywords:["photo","japan","asia","tsukimi"],char:"🎑",fitzpatrick_scale:false,category:"travel_and_places"},mountain:{keywords:["photo","nature","environment"],char:"⛰",fitzpatrick_scale:false,category:"travel_and_places"},mountain_snow:{keywords:["photo","nature","environment","winter","cold"],char:"🏔",fitzpatrick_scale:false,category:"travel_and_places"},mount_fuji:{keywords:["photo","mountain","nature","japanese"],char:"🗻",fitzpatrick_scale:false,category:"travel_and_places"},volcano:{keywords:["photo","nature","disaster"],char:"🌋",fitzpatrick_scale:false,category:"travel_and_places"},japan:{keywords:["nation","country","japanese","asia"],char:"🗾",fitzpatrick_scale:false,category:"travel_and_places"},camping:{keywords:["photo","outdoors","tent"],char:"🏕",fitzpatrick_scale:false,category:"travel_and_places"},tent:{keywords:["photo","camping","outdoors"],char:"⛺",fitzpatrick_scale:false,category:"travel_and_places"},national_park:{keywords:["photo","environment","nature"],char:"🏞",fitzpatrick_scale:false,category:"travel_and_places"},motorway:{keywords:["road","cupertino","interstate","highway"],char:"🛣",fitzpatrick_scale:false,category:"travel_and_places"},railway_track:{keywords:["train","transportation"],char:"🛤",fitzpatrick_scale:false,category:"travel_and_places"},sunrise:{keywords:["morning","view","vacation","photo"],char:"🌅",fitzpatrick_scale:false,category:"travel_and_places"},sunrise_over_mountains:{keywords:["view","vacation","photo"],char:"🌄",fitzpatrick_scale:false,category:"travel_and_places"},desert:{keywords:["photo","warm","saharah"],char:"🏜",fitzpatrick_scale:false,category:"travel_and_places"},beach_umbrella:{keywords:["weather","summer","sunny","sand","mojito"],char:"🏖",fitzpatrick_scale:false,category:"travel_and_places"},desert_island:{keywords:["photo","tropical","mojito"],char:"🏝",fitzpatrick_scale:false,category:"travel_and_places"},city_sunrise:{keywords:["photo","good morning","dawn"],char:"🌇",fitzpatrick_scale:false,category:"travel_and_places"},city_sunset:{keywords:["photo","evening","sky","buildings"],char:"🌆",fitzpatrick_scale:false,category:"travel_and_places"},cityscape:{keywords:["photo","night life","urban"],char:"🏙",fitzpatrick_scale:false,category:"travel_and_places"},night_with_stars:{keywords:["evening","city","downtown"],char:"🌃",fitzpatrick_scale:false,category:"travel_and_places"},bridge_at_night:{keywords:["photo","sanfrancisco"],char:"🌉",fitzpatrick_scale:false,category:"travel_and_places"},milky_way:{keywords:["photo","space","stars"],char:"🌌",fitzpatrick_scale:false,category:"travel_and_places"},stars:{keywords:["night","photo"],char:"🌠",fitzpatrick_scale:false,category:"travel_and_places"},sparkler:{keywords:["stars","night","shine"],char:"🎇",fitzpatrick_scale:false,category:"travel_and_places"},fireworks:{keywords:["photo","festival","carnival","congratulations"],char:"🎆",fitzpatrick_scale:false,category:"travel_and_places"},rainbow:{keywords:["nature","happy","unicorn_face","photo","sky","spring"],char:"🌈",fitzpatrick_scale:false,category:"travel_and_places"},houses:{keywords:["buildings","photo"],char:"🏘",fitzpatrick_scale:false,category:"travel_and_places"},european_castle:{keywords:["building","royalty","history"],char:"🏰",fitzpatrick_scale:false,category:"travel_and_places"},japanese_castle:{keywords:["photo","building"],char:"🏯",fitzpatrick_scale:false,category:"travel_and_places"},stadium:{keywords:["photo","place","sports","concert","venue"],char:"🏟",fitzpatrick_scale:false,category:"travel_and_places"},statue_of_liberty:{keywords:["american","newyork"],char:"🗽",fitzpatrick_scale:false,category:"travel_and_places"},house:{keywords:["building","home"],char:"🏠",fitzpatrick_scale:false,category:"travel_and_places"},house_with_garden:{keywords:["home","plant","nature"],char:"🏡",fitzpatrick_scale:false,category:"travel_and_places"},derelict_house:{keywords:["abandon","evict","broken","building"],char:"🏚",fitzpatrick_scale:false,category:"travel_and_places"},office:{keywords:["building","bureau","work"],char:"🏢",fitzpatrick_scale:false,category:"travel_and_places"},department_store:{keywords:["building","shopping","mall"],char:"🏬",fitzpatrick_scale:false,category:"travel_and_places"},post_office:{keywords:["building","envelope","communication"],char:"🏣",fitzpatrick_scale:false,category:"travel_and_places"},european_post_office:{keywords:["building","email"],char:"🏤",fitzpatrick_scale:false,category:"travel_and_places"},hospital:{keywords:["building","health","surgery","doctor"],char:"🏥",fitzpatrick_scale:false,category:"travel_and_places"},bank:{keywords:["building","money","sales","cash","business","enterprise"],char:"🏦",fitzpatrick_scale:false,category:"travel_and_places"},hotel:{keywords:["building","accomodation","checkin"],char:"🏨",fitzpatrick_scale:false,category:"travel_and_places"},convenience_store:{keywords:["building","shopping","groceries"],char:"🏪",fitzpatrick_scale:false,category:"travel_and_places"},school:{keywords:["building","student","education","learn","teach"],char:"🏫",fitzpatrick_scale:false,category:"travel_and_places"},love_hotel:{keywords:["like","affection","dating"],char:"🏩",fitzpatrick_scale:false,category:"travel_and_places"},wedding:{keywords:["love","like","affection","couple","marriage","bride","groom"],char:"💒",fitzpatrick_scale:false,category:"travel_and_places"},classical_building:{keywords:["art","culture","history"],char:"🏛",fitzpatrick_scale:false,category:"travel_and_places"},church:{keywords:["building","religion","christ"],char:"⛪",fitzpatrick_scale:false,category:"travel_and_places"},mosque:{keywords:["islam","worship","minaret"],char:"🕌",fitzpatrick_scale:false,category:"travel_and_places"},synagogue:{keywords:["judaism","worship","temple","jewish"],char:"🕍",fitzpatrick_scale:false,category:"travel_and_places"},kaaba:{keywords:["mecca","mosque","islam"],char:"🕋",fitzpatrick_scale:false,category:"travel_and_places"},shinto_shrine:{keywords:["temple","japan","kyoto"],char:"⛩",fitzpatrick_scale:false,category:"travel_and_places"},watch:{keywords:["time","accessories"],char:"⌚",fitzpatrick_scale:false,category:"objects"},iphone:{keywords:["technology","apple","gadgets","dial"],char:"📱",fitzpatrick_scale:false,category:"objects"},calling:{keywords:["iphone","incoming"],char:"📲",fitzpatrick_scale:false,category:"objects"},computer:{keywords:["technology","laptop","screen","display","monitor"],char:"💻",fitzpatrick_scale:false,category:"objects"},keyboard:{keywords:["technology","computer","type","input","text"],char:"⌨",fitzpatrick_scale:false,category:"objects"},desktop_computer:{keywords:["technology","computing","screen"],char:"🖥",fitzpatrick_scale:false,category:"objects"},printer:{keywords:["paper","ink"],char:"🖨",fitzpatrick_scale:false,category:"objects"},computer_mouse:{keywords:["click"],char:"🖱",fitzpatrick_scale:false,category:"objects"},trackball:{keywords:["technology","trackpad"],char:"🖲",fitzpatrick_scale:false,category:"objects"},joystick:{keywords:["game","play"],char:"🕹",fitzpatrick_scale:false,category:"objects"},clamp:{keywords:["tool"],char:"🗜",fitzpatrick_scale:false,category:"objects"},minidisc:{keywords:["technology","record","data","disk","90s"],char:"💽",fitzpatrick_scale:false,category:"objects"},floppy_disk:{keywords:["oldschool","technology","save","90s","80s"],char:"💾",fitzpatrick_scale:false,category:"objects"},cd:{keywords:["technology","dvd","disk","disc","90s"],char:"💿",fitzpatrick_scale:false,category:"objects"},dvd:{keywords:["cd","disk","disc"],char:"📀",fitzpatrick_scale:false,category:"objects"},vhs:{keywords:["record","video","oldschool","90s","80s"],char:"📼",fitzpatrick_scale:false,category:"objects"},camera:{keywords:["gadgets","photography"],char:"📷",fitzpatrick_scale:false,category:"objects"},camera_flash:{keywords:["photography","gadgets"],char:"📸",fitzpatrick_scale:false,category:"objects"},video_camera:{keywords:["film","record"],char:"📹",fitzpatrick_scale:false,category:"objects"},movie_camera:{keywords:["film","record"],char:"🎥",fitzpatrick_scale:false,category:"objects"},film_projector:{keywords:["video","tape","record","movie"],char:"📽",fitzpatrick_scale:false,category:"objects"},film_strip:{keywords:["movie"],char:"🎞",fitzpatrick_scale:false,category:"objects"},telephone_receiver:{keywords:["technology","communication","dial"],char:"📞",fitzpatrick_scale:false,category:"objects"},phone:{keywords:["technology","communication","dial","telephone"],char:"☎️",fitzpatrick_scale:false,category:"objects"},pager:{keywords:["bbcall","oldschool","90s"],char:"📟",fitzpatrick_scale:false,category:"objects"},fax:{keywords:["communication","technology"],char:"📠",fitzpatrick_scale:false,category:"objects"},tv:{keywords:["technology","program","oldschool","show","television"],char:"📺",fitzpatrick_scale:false,category:"objects"},radio:{keywords:["communication","music","podcast","program"],char:"📻",fitzpatrick_scale:false,category:"objects"},studio_microphone:{keywords:["sing","recording","artist","talkshow"],char:"🎙",fitzpatrick_scale:false,category:"objects"},level_slider:{keywords:["scale"],char:"🎚",fitzpatrick_scale:false,category:"objects"},control_knobs:{keywords:["dial"],char:"🎛",fitzpatrick_scale:false,category:"objects"},compass:{keywords:["magnetic","navigation","orienteering"],char:"🧭",fitzpatrick_scale:false,category:"objects"},stopwatch:{keywords:["time","deadline"],char:"⏱",fitzpatrick_scale:false,category:"objects"},timer_clock:{keywords:["alarm"],char:"⏲",fitzpatrick_scale:false,category:"objects"},alarm_clock:{keywords:["time","wake"],char:"⏰",fitzpatrick_scale:false,category:"objects"},mantelpiece_clock:{keywords:["time"],char:"🕰",fitzpatrick_scale:false,category:"objects"},hourglass_flowing_sand:{keywords:["oldschool","time","countdown"],char:"⏳",fitzpatrick_scale:false,category:"objects"},hourglass:{keywords:["time","clock","oldschool","limit","exam","quiz","test"],char:"⌛",fitzpatrick_scale:false,category:"objects"},satellite:{keywords:["communication","future","radio","space"],char:"📡",fitzpatrick_scale:false,category:"objects"},battery:{keywords:["power","energy","sustain"],char:"🔋",fitzpatrick_scale:false,category:"objects"},electric_plug:{keywords:["charger","power"],char:"🔌",fitzpatrick_scale:false,category:"objects"},bulb:{keywords:["light","electricity","idea"],char:"💡",fitzpatrick_scale:false,category:"objects"},flashlight:{keywords:["dark","camping","sight","night"],char:"🔦",fitzpatrick_scale:false,category:"objects"},candle:{keywords:["fire","wax"],char:"🕯",fitzpatrick_scale:false,category:"objects"},fire_extinguisher:{keywords:["quench"],char:"🧯",fitzpatrick_scale:false,category:"objects"},wastebasket:{keywords:["bin","trash","rubbish","garbage","toss"],char:"🗑",fitzpatrick_scale:false,category:"objects"},oil_drum:{keywords:["barrell"],char:"🛢",fitzpatrick_scale:false,category:"objects"},money_with_wings:{keywords:["dollar","bills","payment","sale"],char:"💸",fitzpatrick_scale:false,category:"objects"},dollar:{keywords:["money","sales","bill","currency"],char:"💵",fitzpatrick_scale:false,category:"objects"},yen:{keywords:["money","sales","japanese","dollar","currency"],char:"💴",fitzpatrick_scale:false,category:"objects"},euro:{keywords:["money","sales","dollar","currency"],char:"💶",fitzpatrick_scale:false,category:"objects"},pound:{keywords:["british","sterling","money","sales","bills","uk","england","currency"],char:"💷",fitzpatrick_scale:false,category:"objects"},moneybag:{keywords:["dollar","payment","coins","sale"],char:"💰",fitzpatrick_scale:false,category:"objects"},credit_card:{keywords:["money","sales","dollar","bill","payment","shopping"],char:"💳",fitzpatrick_scale:false,category:"objects"},gem:{keywords:["blue","ruby","diamond","jewelry"],char:"💎",fitzpatrick_scale:false,category:"objects"},balance_scale:{keywords:["law","fairness","weight"],char:"⚖",fitzpatrick_scale:false,category:"objects"},toolbox:{keywords:["tools","diy","fix","maintainer","mechanic"],char:"🧰",fitzpatrick_scale:false,category:"objects"},wrench:{keywords:["tools","diy","ikea","fix","maintainer"],char:"🔧",fitzpatrick_scale:false,category:"objects"},hammer:{keywords:["tools","build","create"],char:"🔨",fitzpatrick_scale:false,category:"objects"},hammer_and_pick:{keywords:["tools","build","create"],char:"⚒",fitzpatrick_scale:false,category:"objects"},hammer_and_wrench:{keywords:["tools","build","create"],char:"🛠",fitzpatrick_scale:false,category:"objects"},pick:{keywords:["tools","dig"],char:"⛏",fitzpatrick_scale:false,category:"objects"},nut_and_bolt:{keywords:["handy","tools","fix"],char:"🔩",fitzpatrick_scale:false,category:"objects"},gear:{keywords:["cog"],char:"⚙",fitzpatrick_scale:false,category:"objects"},brick:{keywords:["bricks"],char:"🧱",fitzpatrick_scale:false,category:"objects"},chains:{keywords:["lock","arrest"],char:"⛓",fitzpatrick_scale:false,category:"objects"},magnet:{keywords:["attraction","magnetic"],char:"🧲",fitzpatrick_scale:false,category:"objects"},gun:{keywords:["violence","weapon","pistol","revolver"],char:"🔫",fitzpatrick_scale:false,category:"objects"},bomb:{keywords:["boom","explode","explosion","terrorism"],char:"💣",fitzpatrick_scale:false,category:"objects"},firecracker:{keywords:["dynamite","boom","explode","explosion","explosive"],char:"🧨",fitzpatrick_scale:false,category:"objects"},hocho:{keywords:["knife","blade","cutlery","kitchen","weapon"],char:"🔪",fitzpatrick_scale:false,category:"objects"},dagger:{keywords:["weapon"],char:"🗡",fitzpatrick_scale:false,category:"objects"},crossed_swords:{keywords:["weapon"],char:"⚔",fitzpatrick_scale:false,category:"objects"},shield:{keywords:["protection","security"],char:"🛡",fitzpatrick_scale:false,category:"objects"},smoking:{keywords:["kills","tobacco","cigarette","joint","smoke"],char:"🚬",fitzpatrick_scale:false,category:"objects"},skull_and_crossbones:{keywords:["poison","danger","deadly","scary","death","pirate","evil"],char:"☠",fitzpatrick_scale:false,category:"objects"},coffin:{keywords:["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],char:"⚰",fitzpatrick_scale:false,category:"objects"},funeral_urn:{keywords:["dead","die","death","rip","ashes"],char:"⚱",fitzpatrick_scale:false,category:"objects"},amphora:{keywords:["vase","jar"],char:"🏺",fitzpatrick_scale:false,category:"objects"},crystal_ball:{keywords:["disco","party","magic","circus","fortune_teller"],char:"🔮",fitzpatrick_scale:false,category:"objects"},prayer_beads:{keywords:["dhikr","religious"],char:"📿",fitzpatrick_scale:false,category:"objects"},nazar_amulet:{keywords:["bead","charm"],char:"🧿",fitzpatrick_scale:false,category:"objects"},barber:{keywords:["hair","salon","style"],char:"💈",fitzpatrick_scale:false,category:"objects"},alembic:{keywords:["distilling","science","experiment","chemistry"],char:"⚗",fitzpatrick_scale:false,category:"objects"},telescope:{keywords:["stars","space","zoom","science","astronomy"],char:"🔭",fitzpatrick_scale:false,category:"objects"},microscope:{keywords:["laboratory","experiment","zoomin","science","study"],char:"🔬",fitzpatrick_scale:false,category:"objects"},hole:{keywords:["embarrassing"],char:"🕳",fitzpatrick_scale:false,category:"objects"},pill:{keywords:["health","medicine","doctor","pharmacy","drug"],char:"💊",fitzpatrick_scale:false,category:"objects"},syringe:{keywords:["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],char:"💉",fitzpatrick_scale:false,category:"objects"},dna:{keywords:["biologist","genetics","life"],char:"🧬",fitzpatrick_scale:false,category:"objects"},microbe:{keywords:["amoeba","bacteria","germs"],char:"🦠",fitzpatrick_scale:false,category:"objects"},petri_dish:{keywords:["bacteria","biology","culture","lab"],char:"🧫",fitzpatrick_scale:false,category:"objects"},test_tube:{keywords:["chemistry","experiment","lab","science"],char:"🧪",fitzpatrick_scale:false,category:"objects"},thermometer:{keywords:["weather","temperature","hot","cold"],char:"🌡",fitzpatrick_scale:false,category:"objects"},broom:{keywords:["cleaning","sweeping","witch"],char:"🧹",fitzpatrick_scale:false,category:"objects"},basket:{keywords:["laundry"],char:"🧺",fitzpatrick_scale:false,category:"objects"},toilet_paper:{keywords:["roll"],char:"🧻",fitzpatrick_scale:false,category:"objects"},label:{keywords:["sale","tag"],char:"🏷",fitzpatrick_scale:false,category:"objects"},bookmark:{keywords:["favorite","label","save"],char:"🔖",fitzpatrick_scale:false,category:"objects"},toilet:{keywords:["restroom","wc","washroom","bathroom","potty"],char:"🚽",fitzpatrick_scale:false,category:"objects"},shower:{keywords:["clean","water","bathroom"],char:"🚿",fitzpatrick_scale:false,category:"objects"},bathtub:{keywords:["clean","shower","bathroom"],char:"🛁",fitzpatrick_scale:false,category:"objects"},soap:{keywords:["bar","bathing","cleaning","lather"],char:"🧼",fitzpatrick_scale:false,category:"objects"},sponge:{keywords:["absorbing","cleaning","porous"],char:"🧽",fitzpatrick_scale:false,category:"objects"},lotion_bottle:{keywords:["moisturizer","sunscreen"],char:"🧴",fitzpatrick_scale:false,category:"objects"},key:{keywords:["lock","door","password"],char:"🔑",fitzpatrick_scale:false,category:"objects"},old_key:{keywords:["lock","door","password"],char:"🗝",fitzpatrick_scale:false,category:"objects"},couch_and_lamp:{keywords:["read","chill"],char:"🛋",fitzpatrick_scale:false,category:"objects"},sleeping_bed:{keywords:["bed","rest"],char:"🛌",fitzpatrick_scale:true,category:"objects"},bed:{keywords:["sleep","rest"],char:"🛏",fitzpatrick_scale:false,category:"objects"},door:{keywords:["house","entry","exit"],char:"🚪",fitzpatrick_scale:false,category:"objects"},bellhop_bell:{keywords:["service"],char:"🛎",fitzpatrick_scale:false,category:"objects"},teddy_bear:{keywords:["plush","stuffed"],char:"🧸",fitzpatrick_scale:false,category:"objects"},framed_picture:{keywords:["photography"],char:"🖼",fitzpatrick_scale:false,category:"objects"},world_map:{keywords:["location","direction"],char:"🗺",fitzpatrick_scale:false,category:"objects"},parasol_on_ground:{keywords:["weather","summer"],char:"⛱",fitzpatrick_scale:false,category:"objects"},moyai:{keywords:["rock","easter island","moai"],char:"🗿",fitzpatrick_scale:false,category:"objects"},shopping:{keywords:["mall","buy","purchase"],char:"🛍",fitzpatrick_scale:false,category:"objects"},shopping_cart:{keywords:["trolley"],char:"🛒",fitzpatrick_scale:false,category:"objects"},balloon:{keywords:["party","celebration","birthday","circus"],char:"🎈",fitzpatrick_scale:false,category:"objects"},flags:{keywords:["fish","japanese","koinobori","carp","banner"],char:"🎏",fitzpatrick_scale:false,category:"objects"},ribbon:{keywords:["decoration","pink","girl","bowtie"],char:"🎀",fitzpatrick_scale:false,category:"objects"},gift:{keywords:["present","birthday","christmas","xmas"],char:"🎁",fitzpatrick_scale:false,category:"objects"},confetti_ball:{keywords:["festival","party","birthday","circus"],char:"🎊",fitzpatrick_scale:false,category:"objects"},tada:{keywords:["party","congratulations","birthday","magic","circus","celebration"],char:"🎉",fitzpatrick_scale:false,category:"objects"},dolls:{keywords:["japanese","toy","kimono"],char:"🎎",fitzpatrick_scale:false,category:"objects"},wind_chime:{keywords:["nature","ding","spring","bell"],char:"🎐",fitzpatrick_scale:false,category:"objects"},crossed_flags:{keywords:["japanese","nation","country","border"],char:"🎌",fitzpatrick_scale:false,category:"objects"},izakaya_lantern:{keywords:["light","paper","halloween","spooky"],char:"🏮",fitzpatrick_scale:false,category:"objects"},red_envelope:{keywords:["gift"],char:"🧧",fitzpatrick_scale:false,category:"objects"},email:{keywords:["letter","postal","inbox","communication"],char:"✉️",fitzpatrick_scale:false,category:"objects"},envelope_with_arrow:{keywords:["email","communication"],char:"📩",fitzpatrick_scale:false,category:"objects"},incoming_envelope:{keywords:["email","inbox"],char:"📨",fitzpatrick_scale:false,category:"objects"},"e-mail":{keywords:["communication","inbox"],char:"📧",fitzpatrick_scale:false,category:"objects"},love_letter:{keywords:["email","like","affection","envelope","valentines"],char:"💌",fitzpatrick_scale:false,category:"objects"},postbox:{keywords:["email","letter","envelope"],char:"📮",fitzpatrick_scale:false,category:"objects"},mailbox_closed:{keywords:["email","communication","inbox"],char:"📪",fitzpatrick_scale:false,category:"objects"},mailbox:{keywords:["email","inbox","communication"],char:"📫",fitzpatrick_scale:false,category:"objects"},mailbox_with_mail:{keywords:["email","inbox","communication"],char:"📬",fitzpatrick_scale:false,category:"objects"},mailbox_with_no_mail:{keywords:["email","inbox"],char:"📭",fitzpatrick_scale:false,category:"objects"},package:{keywords:["mail","gift","cardboard","box","moving"],char:"📦",fitzpatrick_scale:false,category:"objects"},postal_horn:{keywords:["instrument","music"],char:"📯",fitzpatrick_scale:false,category:"objects"},inbox_tray:{keywords:["email","documents"],char:"📥",fitzpatrick_scale:false,category:"objects"},outbox_tray:{keywords:["inbox","email"],char:"📤",fitzpatrick_scale:false,category:"objects"},scroll:{keywords:["documents","ancient","history","paper"],char:"📜",fitzpatrick_scale:false,category:"objects"},page_with_curl:{keywords:["documents","office","paper"],char:"📃",fitzpatrick_scale:false,category:"objects"},bookmark_tabs:{keywords:["favorite","save","order","tidy"],char:"📑",fitzpatrick_scale:false,category:"objects"},receipt:{keywords:["accounting","expenses"],char:"🧾",fitzpatrick_scale:false,category:"objects"},bar_chart:{keywords:["graph","presentation","stats"],char:"📊",fitzpatrick_scale:false,category:"objects"},chart_with_upwards_trend:{keywords:["graph","presentation","stats","recovery","business","economics","money","sales","good","success"],char:"📈",fitzpatrick_scale:false,category:"objects"},chart_with_downwards_trend:{keywords:["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],char:"📉",fitzpatrick_scale:false,category:"objects"},page_facing_up:{keywords:["documents","office","paper","information"],char:"📄",fitzpatrick_scale:false,category:"objects"},date:{keywords:["calendar","schedule"],char:"📅",fitzpatrick_scale:false,category:"objects"},calendar:{keywords:["schedule","date","planning"],char:"📆",fitzpatrick_scale:false,category:"objects"},spiral_calendar:{keywords:["date","schedule","planning"],char:"🗓",fitzpatrick_scale:false,category:"objects"},card_index:{keywords:["business","stationery"],char:"📇",fitzpatrick_scale:false,category:"objects"},card_file_box:{keywords:["business","stationery"],char:"🗃",fitzpatrick_scale:false,category:"objects"},ballot_box:{keywords:["election","vote"],char:"🗳",fitzpatrick_scale:false,category:"objects"},file_cabinet:{keywords:["filing","organizing"],char:"🗄",fitzpatrick_scale:false,category:"objects"},clipboard:{keywords:["stationery","documents"],char:"📋",fitzpatrick_scale:false,category:"objects"},spiral_notepad:{keywords:["memo","stationery"],char:"🗒",fitzpatrick_scale:false,category:"objects"},file_folder:{keywords:["documents","business","office"],char:"📁",fitzpatrick_scale:false,category:"objects"},open_file_folder:{keywords:["documents","load"],char:"📂",fitzpatrick_scale:false,category:"objects"},card_index_dividers:{keywords:["organizing","business","stationery"],char:"🗂",fitzpatrick_scale:false,category:"objects"},newspaper_roll:{keywords:["press","headline"],char:"🗞",fitzpatrick_scale:false,category:"objects"},newspaper:{keywords:["press","headline"],char:"📰",fitzpatrick_scale:false,category:"objects"},notebook:{keywords:["stationery","record","notes","paper","study"],char:"📓",fitzpatrick_scale:false,category:"objects"},closed_book:{keywords:["read","library","knowledge","textbook","learn"],char:"📕",fitzpatrick_scale:false,category:"objects"},green_book:{keywords:["read","library","knowledge","study"],char:"📗",fitzpatrick_scale:false,category:"objects"},blue_book:{keywords:["read","library","knowledge","learn","study"],char:"📘",fitzpatrick_scale:false,category:"objects"},orange_book:{keywords:["read","library","knowledge","textbook","study"],char:"📙",fitzpatrick_scale:false,category:"objects"},notebook_with_decorative_cover:{keywords:["classroom","notes","record","paper","study"],char:"📔",fitzpatrick_scale:false,category:"objects"},ledger:{keywords:["notes","paper"],char:"📒",fitzpatrick_scale:false,category:"objects"},books:{keywords:["literature","library","study"],char:"📚",fitzpatrick_scale:false,category:"objects"},open_book:{keywords:["book","read","library","knowledge","literature","learn","study"],char:"📖",fitzpatrick_scale:false,category:"objects"},safety_pin:{keywords:["diaper"],char:"🧷",fitzpatrick_scale:false,category:"objects"},link:{keywords:["rings","url"],char:"🔗",fitzpatrick_scale:false,category:"objects"},paperclip:{keywords:["documents","stationery"],char:"📎",fitzpatrick_scale:false,category:"objects"},paperclips:{keywords:["documents","stationery"],char:"🖇",fitzpatrick_scale:false,category:"objects"},scissors:{keywords:["stationery","cut"],char:"✂️",fitzpatrick_scale:false,category:"objects"},triangular_ruler:{keywords:["stationery","math","architect","sketch"],char:"📐",fitzpatrick_scale:false,category:"objects"},straight_ruler:{keywords:["stationery","calculate","length","math","school","drawing","architect","sketch"],char:"📏",fitzpatrick_scale:false,category:"objects"},abacus:{keywords:["calculation"],char:"🧮",fitzpatrick_scale:false,category:"objects"},pushpin:{keywords:["stationery","mark","here"],char:"📌",fitzpatrick_scale:false,category:"objects"},round_pushpin:{keywords:["stationery","location","map","here"],char:"📍",fitzpatrick_scale:false,category:"objects"},triangular_flag_on_post:{keywords:["mark","milestone","place"],char:"🚩",fitzpatrick_scale:false,category:"objects"},white_flag:{keywords:["losing","loser","lost","surrender","give up","fail"],char:"🏳",fitzpatrick_scale:false,category:"objects"},black_flag:{keywords:["pirate"],char:"🏴",fitzpatrick_scale:false,category:"objects"},rainbow_flag:{keywords:["flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"],char:"🏳️‍🌈",fitzpatrick_scale:false,category:"objects"},closed_lock_with_key:{keywords:["security","privacy"],char:"🔐",fitzpatrick_scale:false,category:"objects"},lock:{keywords:["security","password","padlock"],char:"🔒",fitzpatrick_scale:false,category:"objects"},unlock:{keywords:["privacy","security"],char:"🔓",fitzpatrick_scale:false,category:"objects"},lock_with_ink_pen:{keywords:["security","secret"],char:"🔏",fitzpatrick_scale:false,category:"objects"},pen:{keywords:["stationery","writing","write"],char:"🖊",fitzpatrick_scale:false,category:"objects"},fountain_pen:{keywords:["stationery","writing","write"],char:"🖋",fitzpatrick_scale:false,category:"objects"},black_nib:{keywords:["pen","stationery","writing","write"],char:"✒️",fitzpatrick_scale:false,category:"objects"},memo:{keywords:["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"],char:"📝",fitzpatrick_scale:false,category:"objects"},pencil2:{keywords:["stationery","write","paper","writing","school","study"],char:"✏️",fitzpatrick_scale:false,category:"objects"},crayon:{keywords:["drawing","creativity"],char:"🖍",fitzpatrick_scale:false,category:"objects"},paintbrush:{keywords:["drawing","creativity","art"],char:"🖌",fitzpatrick_scale:false,category:"objects"},mag:{keywords:["search","zoom","find","detective"],char:"🔍",fitzpatrick_scale:false,category:"objects"},mag_right:{keywords:["search","zoom","find","detective"],char:"🔎",fitzpatrick_scale:false,category:"objects"},heart:{keywords:["love","like","valentines"],char:"❤️",fitzpatrick_scale:false,category:"symbols"},orange_heart:{keywords:["love","like","affection","valentines"],char:"🧡",fitzpatrick_scale:false,category:"symbols"},yellow_heart:{keywords:["love","like","affection","valentines"],char:"💛",fitzpatrick_scale:false,category:"symbols"},green_heart:{keywords:["love","like","affection","valentines"],char:"💚",fitzpatrick_scale:false,category:"symbols"},blue_heart:{keywords:["love","like","affection","valentines"],char:"💙",fitzpatrick_scale:false,category:"symbols"},purple_heart:{keywords:["love","like","affection","valentines"],char:"💜",fitzpatrick_scale:false,category:"symbols"},black_heart:{keywords:["evil"],char:"🖤",fitzpatrick_scale:false,category:"symbols"},broken_heart:{keywords:["sad","sorry","break","heart","heartbreak"],char:"💔",fitzpatrick_scale:false,category:"symbols"},heavy_heart_exclamation:{keywords:["decoration","love"],char:"❣",fitzpatrick_scale:false,category:"symbols"},two_hearts:{keywords:["love","like","affection","valentines","heart"],char:"💕",fitzpatrick_scale:false,category:"symbols"},revolving_hearts:{keywords:["love","like","affection","valentines"],char:"💞",fitzpatrick_scale:false,category:"symbols"},heartbeat:{keywords:["love","like","affection","valentines","pink","heart"],char:"💓",fitzpatrick_scale:false,category:"symbols"},heartpulse:{keywords:["like","love","affection","valentines","pink"],char:"💗",fitzpatrick_scale:false,category:"symbols"},sparkling_heart:{keywords:["love","like","affection","valentines"],char:"💖",fitzpatrick_scale:false,category:"symbols"},cupid:{keywords:["love","like","heart","affection","valentines"],char:"💘",fitzpatrick_scale:false,category:"symbols"},gift_heart:{keywords:["love","valentines"],char:"💝",fitzpatrick_scale:false,category:"symbols"},heart_decoration:{keywords:["purple-square","love","like"],char:"💟",fitzpatrick_scale:false,category:"symbols"},peace_symbol:{keywords:["hippie"],char:"☮",fitzpatrick_scale:false,category:"symbols"},latin_cross:{keywords:["christianity"],char:"✝",fitzpatrick_scale:false,category:"symbols"},star_and_crescent:{keywords:["islam"],char:"☪",fitzpatrick_scale:false,category:"symbols"},om:{keywords:["hinduism","buddhism","sikhism","jainism"],char:"🕉",fitzpatrick_scale:false,category:"symbols"},wheel_of_dharma:{keywords:["hinduism","buddhism","sikhism","jainism"],char:"☸",fitzpatrick_scale:false,category:"symbols"},star_of_david:{keywords:["judaism"],char:"✡",fitzpatrick_scale:false,category:"symbols"},six_pointed_star:{keywords:["purple-square","religion","jewish","hexagram"],char:"🔯",fitzpatrick_scale:false,category:"symbols"},menorah:{keywords:["hanukkah","candles","jewish"],char:"🕎",fitzpatrick_scale:false,category:"symbols"},yin_yang:{keywords:["balance"],char:"☯",fitzpatrick_scale:false,category:"symbols"},orthodox_cross:{keywords:["suppedaneum","religion"],char:"☦",fitzpatrick_scale:false,category:"symbols"},place_of_worship:{keywords:["religion","church","temple","prayer"],char:"🛐",fitzpatrick_scale:false,category:"symbols"},ophiuchus:{keywords:["sign","purple-square","constellation","astrology"],char:"⛎",fitzpatrick_scale:false,category:"symbols"},aries:{keywords:["sign","purple-square","zodiac","astrology"],char:"♈",fitzpatrick_scale:false,category:"symbols"},taurus:{keywords:["purple-square","sign","zodiac","astrology"],char:"♉",fitzpatrick_scale:false,category:"symbols"},gemini:{keywords:["sign","zodiac","purple-square","astrology"],char:"♊",fitzpatrick_scale:false,category:"symbols"},cancer:{keywords:["sign","zodiac","purple-square","astrology"],char:"♋",fitzpatrick_scale:false,category:"symbols"},leo:{keywords:["sign","purple-square","zodiac","astrology"],char:"♌",fitzpatrick_scale:false,category:"symbols"},virgo:{keywords:["sign","zodiac","purple-square","astrology"],char:"♍",fitzpatrick_scale:false,category:"symbols"},libra:{keywords:["sign","purple-square","zodiac","astrology"],char:"♎",fitzpatrick_scale:false,category:"symbols"},scorpius:{keywords:["sign","zodiac","purple-square","astrology","scorpio"],char:"♏",fitzpatrick_scale:false,category:"symbols"},sagittarius:{keywords:["sign","zodiac","purple-square","astrology"],char:"♐",fitzpatrick_scale:false,category:"symbols"},capricorn:{keywords:["sign","zodiac","purple-square","astrology"],char:"♑",fitzpatrick_scale:false,category:"symbols"},aquarius:{keywords:["sign","purple-square","zodiac","astrology"],char:"♒",fitzpatrick_scale:false,category:"symbols"},pisces:{keywords:["purple-square","sign","zodiac","astrology"],char:"♓",fitzpatrick_scale:false,category:"symbols"},id:{keywords:["purple-square","words"],char:"🆔",fitzpatrick_scale:false,category:"symbols"},atom_symbol:{keywords:["science","physics","chemistry"],char:"⚛",fitzpatrick_scale:false,category:"symbols"},u7a7a:{keywords:["kanji","japanese","chinese","empty","sky","blue-square"],char:"🈳",fitzpatrick_scale:false,category:"symbols"},u5272:{keywords:["cut","divide","chinese","kanji","pink-square"],char:"🈹",fitzpatrick_scale:false,category:"symbols"},radioactive:{keywords:["nuclear","danger"],char:"☢",fitzpatrick_scale:false,category:"symbols"},biohazard:{keywords:["danger"],char:"☣",fitzpatrick_scale:false,category:"symbols"},mobile_phone_off:{keywords:["mute","orange-square","silence","quiet"],char:"📴",fitzpatrick_scale:false,category:"symbols"},vibration_mode:{keywords:["orange-square","phone"],char:"📳",fitzpatrick_scale:false,category:"symbols"},u6709:{keywords:["orange-square","chinese","have","kanji"],char:"🈶",fitzpatrick_scale:false,category:"symbols"},u7121:{keywords:["nothing","chinese","kanji","japanese","orange-square"],char:"🈚",fitzpatrick_scale:false,category:"symbols"},u7533:{keywords:["chinese","japanese","kanji","orange-square"],char:"🈸",fitzpatrick_scale:false,category:"symbols"},u55b6:{keywords:["japanese","opening hours","orange-square"],char:"🈺",fitzpatrick_scale:false,category:"symbols"},u6708:{keywords:["chinese","month","moon","japanese","orange-square","kanji"],char:"🈷️",fitzpatrick_scale:false,category:"symbols"},eight_pointed_black_star:{keywords:["orange-square","shape","polygon"],char:"✴️",fitzpatrick_scale:false,category:"symbols"},vs:{keywords:["words","orange-square"],char:"🆚",fitzpatrick_scale:false,category:"symbols"},accept:{keywords:["ok","good","chinese","kanji","agree","yes","orange-circle"],char:"🉑",fitzpatrick_scale:false,category:"symbols"},white_flower:{keywords:["japanese","spring"],char:"💮",fitzpatrick_scale:false,category:"symbols"},ideograph_advantage:{keywords:["chinese","kanji","obtain","get","circle"],char:"🉐",fitzpatrick_scale:false,category:"symbols"},secret:{keywords:["privacy","chinese","sshh","kanji","red-circle"],char:"㊙️",fitzpatrick_scale:false,category:"symbols"},congratulations:{keywords:["chinese","kanji","japanese","red-circle"],char:"㊗️",fitzpatrick_scale:false,category:"symbols"},u5408:{keywords:["japanese","chinese","join","kanji","red-square"],char:"🈴",fitzpatrick_scale:false,category:"symbols"},u6e80:{keywords:["full","chinese","japanese","red-square","kanji"],char:"🈵",fitzpatrick_scale:false,category:"symbols"},u7981:{keywords:["kanji","japanese","chinese","forbidden","limit","restricted","red-square"],char:"🈲",fitzpatrick_scale:false,category:"symbols"},a:{keywords:["red-square","alphabet","letter"],char:"🅰️",fitzpatrick_scale:false,category:"symbols"},b:{keywords:["red-square","alphabet","letter"],char:"🅱️",fitzpatrick_scale:false,category:"symbols"},ab:{keywords:["red-square","alphabet"],char:"🆎",fitzpatrick_scale:false,category:"symbols"},cl:{keywords:["alphabet","words","red-square"],char:"🆑",fitzpatrick_scale:false,category:"symbols"},o2:{keywords:["alphabet","red-square","letter"],char:"🅾️",fitzpatrick_scale:false,category:"symbols"},sos:{keywords:["help","red-square","words","emergency","911"],char:"🆘",fitzpatrick_scale:false,category:"symbols"},no_entry:{keywords:["limit","security","privacy","bad","denied","stop","circle"],char:"⛔",fitzpatrick_scale:false,category:"symbols"},name_badge:{keywords:["fire","forbid"],char:"📛",fitzpatrick_scale:false,category:"symbols"},no_entry_sign:{keywords:["forbid","stop","limit","denied","disallow","circle"],char:"🚫",fitzpatrick_scale:false,category:"symbols"},x:{keywords:["no","delete","remove","cancel","red"],char:"❌",fitzpatrick_scale:false,category:"symbols"},o:{keywords:["circle","round"],char:"⭕",fitzpatrick_scale:false,category:"symbols"},stop_sign:{keywords:["stop"],char:"🛑",fitzpatrick_scale:false,category:"symbols"},anger:{keywords:["angry","mad"],char:"💢",fitzpatrick_scale:false,category:"symbols"},hotsprings:{keywords:["bath","warm","relax"],char:"♨️",fitzpatrick_scale:false,category:"symbols"},no_pedestrians:{keywords:["rules","crossing","walking","circle"],char:"🚷",fitzpatrick_scale:false,category:"symbols"},do_not_litter:{keywords:["trash","bin","garbage","circle"],char:"🚯",fitzpatrick_scale:false,category:"symbols"},no_bicycles:{keywords:["cyclist","prohibited","circle"],char:"🚳",fitzpatrick_scale:false,category:"symbols"},"non-potable_water":{keywords:["drink","faucet","tap","circle"],char:"🚱",fitzpatrick_scale:false,category:"symbols"},underage:{keywords:["18","drink","pub","night","minor","circle"],char:"🔞",fitzpatrick_scale:false,category:"symbols"},no_mobile_phones:{keywords:["iphone","mute","circle"],char:"📵",fitzpatrick_scale:false,category:"symbols"},exclamation:{keywords:["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"],char:"❗",fitzpatrick_scale:false,category:"symbols"},grey_exclamation:{keywords:["surprise","punctuation","gray","wow","warning"],char:"❕",fitzpatrick_scale:false,category:"symbols"},question:{keywords:["doubt","confused"],char:"❓",fitzpatrick_scale:false,category:"symbols"},grey_question:{keywords:["doubts","gray","huh","confused"],char:"❔",fitzpatrick_scale:false,category:"symbols"},bangbang:{keywords:["exclamation","surprise"],char:"‼️",fitzpatrick_scale:false,category:"symbols"},interrobang:{keywords:["wat","punctuation","surprise"],char:"⁉️",fitzpatrick_scale:false,category:"symbols"},100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:"💯",fitzpatrick_scale:false,category:"symbols"},low_brightness:{keywords:["sun","afternoon","warm","summer"],char:"🔅",fitzpatrick_scale:false,category:"symbols"},high_brightness:{keywords:["sun","light"],char:"🔆",fitzpatrick_scale:false,category:"symbols"},trident:{keywords:["weapon","spear"],char:"🔱",fitzpatrick_scale:false,category:"symbols"},fleur_de_lis:{keywords:["decorative","scout"],char:"⚜",fitzpatrick_scale:false,category:"symbols"},part_alternation_mark:{keywords:["graph","presentation","stats","business","economics","bad"],char:"〽️",fitzpatrick_scale:false,category:"symbols"},warning:{keywords:["exclamation","wip","alert","error","problem","issue"],char:"⚠️",fitzpatrick_scale:false,category:"symbols"},children_crossing:{keywords:["school","warning","danger","sign","driving","yellow-diamond"],char:"🚸",fitzpatrick_scale:false,category:"symbols"},beginner:{keywords:["badge","shield"],char:"🔰",fitzpatrick_scale:false,category:"symbols"},recycle:{keywords:["arrow","environment","garbage","trash"],char:"♻️",fitzpatrick_scale:false,category:"symbols"},u6307:{keywords:["chinese","point","green-square","kanji"],char:"🈯",fitzpatrick_scale:false,category:"symbols"},chart:{keywords:["green-square","graph","presentation","stats"],char:"💹",fitzpatrick_scale:false,category:"symbols"},sparkle:{keywords:["stars","green-square","awesome","good","fireworks"],char:"❇️",fitzpatrick_scale:false,category:"symbols"},eight_spoked_asterisk:{keywords:["star","sparkle","green-square"],char:"✳️",fitzpatrick_scale:false,category:"symbols"},negative_squared_cross_mark:{keywords:["x","green-square","no","deny"],char:"❎",fitzpatrick_scale:false,category:"symbols"},white_check_mark:{keywords:["green-square","ok","agree","vote","election","answer","tick"],char:"✅",fitzpatrick_scale:false,category:"symbols"},diamond_shape_with_a_dot_inside:{keywords:["jewel","blue","gem","crystal","fancy"],char:"💠",fitzpatrick_scale:false,category:"symbols"},cyclone:{keywords:["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],char:"🌀",fitzpatrick_scale:false,category:"symbols"},loop:{keywords:["tape","cassette"],char:"➿",fitzpatrick_scale:false,category:"symbols"},globe_with_meridians:{keywords:["earth","international","world","internet","interweb","i18n"],char:"🌐",fitzpatrick_scale:false,category:"symbols"},m:{keywords:["alphabet","blue-circle","letter"],char:"Ⓜ️",fitzpatrick_scale:false,category:"symbols"},atm:{keywords:["money","sales","cash","blue-square","payment","bank"],char:"🏧",fitzpatrick_scale:false,category:"symbols"},sa:{keywords:["japanese","blue-square","katakana"],char:"🈂️",fitzpatrick_scale:false,category:"symbols"},passport_control:{keywords:["custom","blue-square"],char:"🛂",fitzpatrick_scale:false,category:"symbols"},customs:{keywords:["passport","border","blue-square"],char:"🛃",fitzpatrick_scale:false,category:"symbols"},baggage_claim:{keywords:["blue-square","airport","transport"],char:"🛄",fitzpatrick_scale:false,category:"symbols"},left_luggage:{keywords:["blue-square","travel"],char:"🛅",fitzpatrick_scale:false,category:"symbols"},wheelchair:{keywords:["blue-square","disabled","a11y","accessibility"],char:"♿",fitzpatrick_scale:false,category:"symbols"},no_smoking:{keywords:["cigarette","blue-square","smell","smoke"],char:"🚭",fitzpatrick_scale:false,category:"symbols"},wc:{keywords:["toilet","restroom","blue-square"],char:"🚾",fitzpatrick_scale:false,category:"symbols"},parking:{keywords:["cars","blue-square","alphabet","letter"],char:"🅿️",fitzpatrick_scale:false,category:"symbols"},potable_water:{keywords:["blue-square","liquid","restroom","cleaning","faucet"],char:"🚰",fitzpatrick_scale:false,category:"symbols"},mens:{keywords:["toilet","restroom","wc","blue-square","gender","male"],char:"🚹",fitzpatrick_scale:false,category:"symbols"},womens:{keywords:["purple-square","woman","female","toilet","loo","restroom","gender"],char:"🚺",fitzpatrick_scale:false,category:"symbols"},baby_symbol:{keywords:["orange-square","child"],char:"🚼",fitzpatrick_scale:false,category:"symbols"},restroom:{keywords:["blue-square","toilet","refresh","wc","gender"],char:"🚻",fitzpatrick_scale:false,category:"symbols"},put_litter_in_its_place:{keywords:["blue-square","sign","human","info"],char:"🚮",fitzpatrick_scale:false,category:"symbols"},cinema:{keywords:["blue-square","record","film","movie","curtain","stage","theater"],char:"🎦",fitzpatrick_scale:false,category:"symbols"},signal_strength:{keywords:["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"],char:"📶",fitzpatrick_scale:false,category:"symbols"},koko:{keywords:["blue-square","here","katakana","japanese","destination"],char:"🈁",fitzpatrick_scale:false,category:"symbols"},ng:{keywords:["blue-square","words","shape","icon"],char:"🆖",fitzpatrick_scale:false,category:"symbols"},ok:{keywords:["good","agree","yes","blue-square"],char:"🆗",fitzpatrick_scale:false,category:"symbols"},up:{keywords:["blue-square","above","high"],char:"🆙",fitzpatrick_scale:false,category:"symbols"},cool:{keywords:["words","blue-square"],char:"🆒",fitzpatrick_scale:false,category:"symbols"},new:{keywords:["blue-square","words","start"],char:"🆕",fitzpatrick_scale:false,category:"symbols"},free:{keywords:["blue-square","words"],char:"🆓",fitzpatrick_scale:false,category:"symbols"},zero:{keywords:["0","numbers","blue-square","null"],char:"0️⃣",fitzpatrick_scale:false,category:"symbols"},one:{keywords:["blue-square","numbers","1"],char:"1️⃣",fitzpatrick_scale:false,category:"symbols"},two:{keywords:["numbers","2","prime","blue-square"],char:"2️⃣",fitzpatrick_scale:false,category:"symbols"},three:{keywords:["3","numbers","prime","blue-square"],char:"3️⃣",fitzpatrick_scale:false,category:"symbols"},four:{keywords:["4","numbers","blue-square"],char:"4️⃣",fitzpatrick_scale:false,category:"symbols"},five:{keywords:["5","numbers","blue-square","prime"],char:"5️⃣",fitzpatrick_scale:false,category:"symbols"},six:{keywords:["6","numbers","blue-square"],char:"6️⃣",fitzpatrick_scale:false,category:"symbols"},seven:{keywords:["7","numbers","blue-square","prime"],char:"7️⃣",fitzpatrick_scale:false,category:"symbols"},eight:{keywords:["8","blue-square","numbers"],char:"8️⃣",fitzpatrick_scale:false,category:"symbols"},nine:{keywords:["blue-square","numbers","9"],char:"9️⃣",fitzpatrick_scale:false,category:"symbols"},keycap_ten:{keywords:["numbers","10","blue-square"],char:"🔟",fitzpatrick_scale:false,category:"symbols"},asterisk:{keywords:["star","keycap"],char:"*⃣",fitzpatrick_scale:false,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:"🔢",fitzpatrick_scale:false,category:"symbols"},eject_button:{keywords:["blue-square"],char:"⏏️",fitzpatrick_scale:false,category:"symbols"},arrow_forward:{keywords:["blue-square","right","direction","play"],char:"▶️",fitzpatrick_scale:false,category:"symbols"},pause_button:{keywords:["pause","blue-square"],char:"⏸",fitzpatrick_scale:false,category:"symbols"},next_track_button:{keywords:["forward","next","blue-square"],char:"⏭",fitzpatrick_scale:false,category:"symbols"},stop_button:{keywords:["blue-square"],char:"⏹",fitzpatrick_scale:false,category:"symbols"},record_button:{keywords:["blue-square"],char:"⏺",fitzpatrick_scale:false,category:"symbols"},play_or_pause_button:{keywords:["blue-square","play","pause"],char:"⏯",fitzpatrick_scale:false,category:"symbols"},previous_track_button:{keywords:["backward"],char:"⏮",fitzpatrick_scale:false,category:"symbols"},fast_forward:{keywords:["blue-square","play","speed","continue"],char:"⏩",fitzpatrick_scale:false,category:"symbols"},rewind:{keywords:["play","blue-square"],char:"⏪",fitzpatrick_scale:false,category:"symbols"},twisted_rightwards_arrows:{keywords:["blue-square","shuffle","music","random"],char:"🔀",fitzpatrick_scale:false,category:"symbols"},repeat:{keywords:["loop","record"],char:"🔁",fitzpatrick_scale:false,category:"symbols"},repeat_one:{keywords:["blue-square","loop"],char:"🔂",fitzpatrick_scale:false,category:"symbols"},arrow_backward:{keywords:["blue-square","left","direction"],char:"◀️",fitzpatrick_scale:false,category:"symbols"},arrow_up_small:{keywords:["blue-square","triangle","direction","point","forward","top"],char:"🔼",fitzpatrick_scale:false,category:"symbols"},arrow_down_small:{keywords:["blue-square","direction","bottom"],char:"🔽",fitzpatrick_scale:false,category:"symbols"},arrow_double_up:{keywords:["blue-square","direction","top"],char:"⏫",fitzpatrick_scale:false,category:"symbols"},arrow_double_down:{keywords:["blue-square","direction","bottom"],char:"⏬",fitzpatrick_scale:false,category:"symbols"},arrow_right:{keywords:["blue-square","next"],char:"➡️",fitzpatrick_scale:false,category:"symbols"},arrow_left:{keywords:["blue-square","previous","back"],char:"⬅️",fitzpatrick_scale:false,category:"symbols"},arrow_up:{keywords:["blue-square","continue","top","direction"],char:"⬆️",fitzpatrick_scale:false,category:"symbols"},arrow_down:{keywords:["blue-square","direction","bottom"],char:"⬇️",fitzpatrick_scale:false,category:"symbols"},arrow_upper_right:{keywords:["blue-square","point","direction","diagonal","northeast"],char:"↗️",fitzpatrick_scale:false,category:"symbols"},arrow_lower_right:{keywords:["blue-square","direction","diagonal","southeast"],char:"↘️",fitzpatrick_scale:false,category:"symbols"},arrow_lower_left:{keywords:["blue-square","direction","diagonal","southwest"],char:"↙️",fitzpatrick_scale:false,category:"symbols"},arrow_upper_left:{keywords:["blue-square","point","direction","diagonal","northwest"],char:"↖️",fitzpatrick_scale:false,category:"symbols"},arrow_up_down:{keywords:["blue-square","direction","way","vertical"],char:"↕️",fitzpatrick_scale:false,category:"symbols"},left_right_arrow:{keywords:["shape","direction","horizontal","sideways"],char:"↔️",fitzpatrick_scale:false,category:"symbols"},arrows_counterclockwise:{keywords:["blue-square","sync","cycle"],char:"🔄",fitzpatrick_scale:false,category:"symbols"},arrow_right_hook:{keywords:["blue-square","return","rotate","direction"],char:"↪️",fitzpatrick_scale:false,category:"symbols"},leftwards_arrow_with_hook:{keywords:["back","return","blue-square","undo","enter"],char:"↩️",fitzpatrick_scale:false,category:"symbols"},arrow_heading_up:{keywords:["blue-square","direction","top"],char:"⤴️",fitzpatrick_scale:false,category:"symbols"},arrow_heading_down:{keywords:["blue-square","direction","bottom"],char:"⤵️",fitzpatrick_scale:false,category:"symbols"},hash:{keywords:["symbol","blue-square","twitter"],char:"#️⃣",fitzpatrick_scale:false,category:"symbols"},information_source:{keywords:["blue-square","alphabet","letter"],char:"ℹ️",fitzpatrick_scale:false,category:"symbols"},abc:{keywords:["blue-square","alphabet"],char:"🔤",fitzpatrick_scale:false,category:"symbols"},abcd:{keywords:["blue-square","alphabet"],char:"🔡",fitzpatrick_scale:false,category:"symbols"},capital_abcd:{keywords:["alphabet","words","blue-square"],char:"🔠",fitzpatrick_scale:false,category:"symbols"},symbols:{keywords:["blue-square","music","note","ampersand","percent","glyphs","characters"],char:"🔣",fitzpatrick_scale:false,category:"symbols"},musical_note:{keywords:["score","tone","sound"],char:"🎵",fitzpatrick_scale:false,category:"symbols"},notes:{keywords:["music","score"],char:"🎶",fitzpatrick_scale:false,category:"symbols"},wavy_dash:{keywords:["draw","line","moustache","mustache","squiggle","scribble"],char:"〰️",fitzpatrick_scale:false,category:"symbols"},curly_loop:{keywords:["scribble","draw","shape","squiggle"],char:"➰",fitzpatrick_scale:false,category:"symbols"},heavy_check_mark:{keywords:["ok","nike","answer","yes","tick"],char:"✔️",fitzpatrick_scale:false,category:"symbols"},arrows_clockwise:{keywords:["sync","cycle","round","repeat"],char:"🔃",fitzpatrick_scale:false,category:"symbols"},heavy_plus_sign:{keywords:["math","calculation","addition","more","increase"],char:"➕",fitzpatrick_scale:false,category:"symbols"},heavy_minus_sign:{keywords:["math","calculation","subtract","less"],char:"➖",fitzpatrick_scale:false,category:"symbols"},heavy_division_sign:{keywords:["divide","math","calculation"],char:"➗",fitzpatrick_scale:false,category:"symbols"},heavy_multiplication_x:{keywords:["math","calculation"],char:"✖️",fitzpatrick_scale:false,category:"symbols"},infinity:{keywords:["forever"],char:"♾",fitzpatrick_scale:false,category:"symbols"},heavy_dollar_sign:{keywords:["money","sales","payment","currency","buck"],char:"💲",fitzpatrick_scale:false,category:"symbols"},currency_exchange:{keywords:["money","sales","dollar","travel"],char:"💱",fitzpatrick_scale:false,category:"symbols"},copyright:{keywords:["ip","license","circle","law","legal"],char:"©️",fitzpatrick_scale:false,category:"symbols"},registered:{keywords:["alphabet","circle"],char:"®️",fitzpatrick_scale:false,category:"symbols"},tm:{keywords:["trademark","brand","law","legal"],char:"™️",fitzpatrick_scale:false,category:"symbols"},end:{keywords:["words","arrow"],char:"🔚",fitzpatrick_scale:false,category:"symbols"},back:{keywords:["arrow","words","return"],char:"🔙",fitzpatrick_scale:false,category:"symbols"},on:{keywords:["arrow","words"],char:"🔛",fitzpatrick_scale:false,category:"symbols"},top:{keywords:["words","blue-square"],char:"🔝",fitzpatrick_scale:false,category:"symbols"},soon:{keywords:["arrow","words"],char:"🔜",fitzpatrick_scale:false,category:"symbols"},ballot_box_with_check:{keywords:["ok","agree","confirm","black-square","vote","election","yes","tick"],char:"☑️",fitzpatrick_scale:false,category:"symbols"},radio_button:{keywords:["input","old","music","circle"],char:"🔘",fitzpatrick_scale:false,category:"symbols"},white_circle:{keywords:["shape","round"],char:"⚪",fitzpatrick_scale:false,category:"symbols"},black_circle:{keywords:["shape","button","round"],char:"⚫",fitzpatrick_scale:false,category:"symbols"},red_circle:{keywords:["shape","error","danger"],char:"🔴",fitzpatrick_scale:false,category:"symbols"},large_blue_circle:{keywords:["shape","icon","button"],char:"🔵",fitzpatrick_scale:false,category:"symbols"},small_orange_diamond:{keywords:["shape","jewel","gem"],char:"🔸",fitzpatrick_scale:false,category:"symbols"},small_blue_diamond:{keywords:["shape","jewel","gem"],char:"🔹",fitzpatrick_scale:false,category:"symbols"},large_orange_diamond:{keywords:["shape","jewel","gem"],char:"🔶",fitzpatrick_scale:false,category:"symbols"},large_blue_diamond:{keywords:["shape","jewel","gem"],char:"🔷",fitzpatrick_scale:false,category:"symbols"},small_red_triangle:{keywords:["shape","direction","up","top"],char:"🔺",fitzpatrick_scale:false,category:"symbols"},black_small_square:{keywords:["shape","icon"],char:"▪️",fitzpatrick_scale:false,category:"symbols"},white_small_square:{keywords:["shape","icon"],char:"▫️",fitzpatrick_scale:false,category:"symbols"},black_large_square:{keywords:["shape","icon","button"],char:"⬛",fitzpatrick_scale:false,category:"symbols"},white_large_square:{keywords:["shape","icon","stone","button"],char:"⬜",fitzpatrick_scale:false,category:"symbols"},small_red_triangle_down:{keywords:["shape","direction","bottom"],char:"🔻",fitzpatrick_scale:false,category:"symbols"},black_medium_square:{keywords:["shape","button","icon"],char:"◼️",fitzpatrick_scale:false,category:"symbols"},white_medium_square:{keywords:["shape","stone","icon"],char:"◻️",fitzpatrick_scale:false,category:"symbols"},black_medium_small_square:{keywords:["icon","shape","button"],char:"◾",fitzpatrick_scale:false,category:"symbols"},white_medium_small_square:{keywords:["shape","stone","icon","button"],char:"◽",fitzpatrick_scale:false,category:"symbols"},black_square_button:{keywords:["shape","input","frame"],char:"🔲",fitzpatrick_scale:false,category:"symbols"},white_square_button:{keywords:["shape","input"],char:"🔳",fitzpatrick_scale:false,category:"symbols"},speaker:{keywords:["sound","volume","silence","broadcast"],char:"🔈",fitzpatrick_scale:false,category:"symbols"},sound:{keywords:["volume","speaker","broadcast"],char:"🔉",fitzpatrick_scale:false,category:"symbols"},loud_sound:{keywords:["volume","noise","noisy","speaker","broadcast"],char:"🔊",fitzpatrick_scale:false,category:"symbols"},mute:{keywords:["sound","volume","silence","quiet"],char:"🔇",fitzpatrick_scale:false,category:"symbols"},mega:{keywords:["sound","speaker","volume"],char:"📣",fitzpatrick_scale:false,category:"symbols"},loudspeaker:{keywords:["volume","sound"],char:"📢",fitzpatrick_scale:false,category:"symbols"},bell:{keywords:["sound","notification","christmas","xmas","chime"],char:"🔔",fitzpatrick_scale:false,category:"symbols"},no_bell:{keywords:["sound","volume","mute","quiet","silent"],char:"🔕",fitzpatrick_scale:false,category:"symbols"},black_joker:{keywords:["poker","cards","game","play","magic"],char:"🃏",fitzpatrick_scale:false,category:"symbols"},mahjong:{keywords:["game","play","chinese","kanji"],char:"🀄",fitzpatrick_scale:false,category:"symbols"},spades:{keywords:["poker","cards","suits","magic"],char:"♠️",fitzpatrick_scale:false,category:"symbols"},clubs:{keywords:["poker","cards","magic","suits"],char:"♣️",fitzpatrick_scale:false,category:"symbols"},hearts:{keywords:["poker","cards","magic","suits"],char:"♥️",fitzpatrick_scale:false,category:"symbols"},diamonds:{keywords:["poker","cards","magic","suits"],char:"♦️",fitzpatrick_scale:false,category:"symbols"},flower_playing_cards:{keywords:["game","sunset","red"],char:"🎴",fitzpatrick_scale:false,category:"symbols"},thought_balloon:{keywords:["bubble","cloud","speech","thinking","dream"],char:"💭",fitzpatrick_scale:false,category:"symbols"},right_anger_bubble:{keywords:["caption","speech","thinking","mad"],char:"🗯",fitzpatrick_scale:false,category:"symbols"},speech_balloon:{keywords:["bubble","words","message","talk","chatting"],char:"💬",fitzpatrick_scale:false,category:"symbols"},left_speech_bubble:{keywords:["words","message","talk","chatting"],char:"🗨",fitzpatrick_scale:false,category:"symbols"},clock1:{keywords:["time","late","early","schedule"],char:"🕐",fitzpatrick_scale:false,category:"symbols"},clock2:{keywords:["time","late","early","schedule"],char:"🕑",fitzpatrick_scale:false,category:"symbols"},clock3:{keywords:["time","late","early","schedule"],char:"🕒",fitzpatrick_scale:false,category:"symbols"},clock4:{keywords:["time","late","early","schedule"],char:"🕓",fitzpatrick_scale:false,category:"symbols"},clock5:{keywords:["time","late","early","schedule"],char:"🕔",fitzpatrick_scale:false,category:"symbols"},clock6:{keywords:["time","late","early","schedule","dawn","dusk"],char:"🕕",fitzpatrick_scale:false,category:"symbols"},clock7:{keywords:["time","late","early","schedule"],char:"🕖",fitzpatrick_scale:false,category:"symbols"},clock8:{keywords:["time","late","early","schedule"],char:"🕗",fitzpatrick_scale:false,category:"symbols"},clock9:{keywords:["time","late","early","schedule"],char:"🕘",fitzpatrick_scale:false,category:"symbols"},clock10:{keywords:["time","late","early","schedule"],char:"🕙",fitzpatrick_scale:false,category:"symbols"},clock11:{keywords:["time","late","early","schedule"],char:"🕚",fitzpatrick_scale:false,category:"symbols"},clock12:{keywords:["time","noon","midnight","midday","late","early","schedule"],char:"🕛",fitzpatrick_scale:false,category:"symbols"},clock130:{keywords:["time","late","early","schedule"],char:"🕜",fitzpatrick_scale:false,category:"symbols"},clock230:{keywords:["time","late","early","schedule"],char:"🕝",fitzpatrick_scale:false,category:"symbols"},clock330:{keywords:["time","late","early","schedule"],char:"🕞",fitzpatrick_scale:false,category:"symbols"},clock430:{keywords:["time","late","early","schedule"],char:"🕟",fitzpatrick_scale:false,category:"symbols"},clock530:{keywords:["time","late","early","schedule"],char:"🕠",fitzpatrick_scale:false,category:"symbols"},clock630:{keywords:["time","late","early","schedule"],char:"🕡",fitzpatrick_scale:false,category:"symbols"},clock730:{keywords:["time","late","early","schedule"],char:"🕢",fitzpatrick_scale:false,category:"symbols"},clock830:{keywords:["time","late","early","schedule"],char:"🕣",fitzpatrick_scale:false,category:"symbols"},clock930:{keywords:["time","late","early","schedule"],char:"🕤",fitzpatrick_scale:false,category:"symbols"},clock1030:{keywords:["time","late","early","schedule"],char:"🕥",fitzpatrick_scale:false,category:"symbols"},clock1130:{keywords:["time","late","early","schedule"],char:"🕦",fitzpatrick_scale:false,category:"symbols"},clock1230:{keywords:["time","late","early","schedule"],char:"🕧",fitzpatrick_scale:false,category:"symbols"},afghanistan:{keywords:["af","flag","nation","country","banner"],char:"🇦🇫",fitzpatrick_scale:false,category:"flags"},aland_islands:{keywords:["Åland","islands","flag","nation","country","banner"],char:"🇦🇽",fitzpatrick_scale:false,category:"flags"},albania:{keywords:["al","flag","nation","country","banner"],char:"🇦🇱",fitzpatrick_scale:false,category:"flags"},algeria:{keywords:["dz","flag","nation","country","banner"],char:"🇩🇿",fitzpatrick_scale:false,category:"flags"},american_samoa:{keywords:["american","ws","flag","nation","country","banner"],char:"🇦🇸",fitzpatrick_scale:false,category:"flags"},andorra:{keywords:["ad","flag","nation","country","banner"],char:"🇦🇩",fitzpatrick_scale:false,category:"flags"},angola:{keywords:["ao","flag","nation","country","banner"],char:"🇦🇴",fitzpatrick_scale:false,category:"flags"},anguilla:{keywords:["ai","flag","nation","country","banner"],char:"🇦🇮",fitzpatrick_scale:false,category:"flags"},antarctica:{keywords:["aq","flag","nation","country","banner"],char:"🇦🇶",fitzpatrick_scale:false,category:"flags"},antigua_barbuda:{keywords:["antigua","barbuda","flag","nation","country","banner"],char:"🇦🇬",fitzpatrick_scale:false,category:"flags"},argentina:{keywords:["ar","flag","nation","country","banner"],char:"🇦🇷",fitzpatrick_scale:false,category:"flags"},armenia:{keywords:["am","flag","nation","country","banner"],char:"🇦🇲",fitzpatrick_scale:false,category:"flags"},aruba:{keywords:["aw","flag","nation","country","banner"],char:"🇦🇼",fitzpatrick_scale:false,category:"flags"},australia:{keywords:["au","flag","nation","country","banner"],char:"🇦🇺",fitzpatrick_scale:false,category:"flags"},austria:{keywords:["at","flag","nation","country","banner"],char:"🇦🇹",fitzpatrick_scale:false,category:"flags"},azerbaijan:{keywords:["az","flag","nation","country","banner"],char:"🇦🇿",fitzpatrick_scale:false,category:"flags"},bahamas:{keywords:["bs","flag","nation","country","banner"],char:"🇧🇸",fitzpatrick_scale:false,category:"flags"},bahrain:{keywords:["bh","flag","nation","country","banner"],char:"🇧🇭",fitzpatrick_scale:false,category:"flags"},bangladesh:{keywords:["bd","flag","nation","country","banner"],char:"🇧🇩",fitzpatrick_scale:false,category:"flags"},barbados:{keywords:["bb","flag","nation","country","banner"],char:"🇧🇧",fitzpatrick_scale:false,category:"flags"},belarus:{keywords:["by","flag","nation","country","banner"],char:"🇧🇾",fitzpatrick_scale:false,category:"flags"},belgium:{keywords:["be","flag","nation","country","banner"],char:"🇧🇪",fitzpatrick_scale:false,category:"flags"},belize:{keywords:["bz","flag","nation","country","banner"],char:"🇧🇿",fitzpatrick_scale:false,category:"flags"},benin:{keywords:["bj","flag","nation","country","banner"],char:"🇧🇯",fitzpatrick_scale:false,category:"flags"},bermuda:{keywords:["bm","flag","nation","country","banner"],char:"🇧🇲",fitzpatrick_scale:false,category:"flags"},bhutan:{keywords:["bt","flag","nation","country","banner"],char:"🇧🇹",fitzpatrick_scale:false,category:"flags"},bolivia:{keywords:["bo","flag","nation","country","banner"],char:"🇧🇴",fitzpatrick_scale:false,category:"flags"},caribbean_netherlands:{keywords:["bonaire","flag","nation","country","banner"],char:"🇧🇶",fitzpatrick_scale:false,category:"flags"},bosnia_herzegovina:{keywords:["bosnia","herzegovina","flag","nation","country","banner"],char:"🇧🇦",fitzpatrick_scale:false,category:"flags"},botswana:{keywords:["bw","flag","nation","country","banner"],char:"🇧🇼",fitzpatrick_scale:false,category:"flags"},brazil:{keywords:["br","flag","nation","country","banner"],char:"🇧🇷",fitzpatrick_scale:false,category:"flags"},british_indian_ocean_territory:{keywords:["british","indian","ocean","territory","flag","nation","country","banner"],char:"🇮🇴",fitzpatrick_scale:false,category:"flags"},british_virgin_islands:{keywords:["british","virgin","islands","bvi","flag","nation","country","banner"],char:"🇻🇬",fitzpatrick_scale:false,category:"flags"},brunei:{keywords:["bn","darussalam","flag","nation","country","banner"],char:"🇧🇳",fitzpatrick_scale:false,category:"flags"},bulgaria:{keywords:["bg","flag","nation","country","banner"],char:"🇧🇬",fitzpatrick_scale:false,category:"flags"},burkina_faso:{keywords:["burkina","faso","flag","nation","country","banner"],char:"🇧🇫",fitzpatrick_scale:false,category:"flags"},burundi:{keywords:["bi","flag","nation","country","banner"],char:"🇧🇮",fitzpatrick_scale:false,category:"flags"},cape_verde:{keywords:["cabo","verde","flag","nation","country","banner"],char:"🇨🇻",fitzpatrick_scale:false,category:"flags"},cambodia:{keywords:["kh","flag","nation","country","banner"],char:"🇰🇭",fitzpatrick_scale:false,category:"flags"},cameroon:{keywords:["cm","flag","nation","country","banner"],char:"🇨🇲",fitzpatrick_scale:false,category:"flags"},canada:{keywords:["ca","flag","nation","country","banner"],char:"🇨🇦",fitzpatrick_scale:false,category:"flags"},canary_islands:{keywords:["canary","islands","flag","nation","country","banner"],char:"🇮🇨",fitzpatrick_scale:false,category:"flags"},cayman_islands:{keywords:["cayman","islands","flag","nation","country","banner"],char:"🇰🇾",fitzpatrick_scale:false,category:"flags"},central_african_republic:{keywords:["central","african","republic","flag","nation","country","banner"],char:"🇨🇫",fitzpatrick_scale:false,category:"flags"},chad:{keywords:["td","flag","nation","country","banner"],char:"🇹🇩",fitzpatrick_scale:false,category:"flags"},chile:{keywords:["flag","nation","country","banner"],char:"🇨🇱",fitzpatrick_scale:false,category:"flags"},cn:{keywords:["china","chinese","prc","flag","country","nation","banner"],char:"🇨🇳",fitzpatrick_scale:false,category:"flags"},christmas_island:{keywords:["christmas","island","flag","nation","country","banner"],char:"🇨🇽",fitzpatrick_scale:false,category:"flags"},cocos_islands:{keywords:["cocos","keeling","islands","flag","nation","country","banner"],char:"🇨🇨",fitzpatrick_scale:false,category:"flags"},colombia:{keywords:["co","flag","nation","country","banner"],char:"🇨🇴",fitzpatrick_scale:false,category:"flags"},comoros:{keywords:["km","flag","nation","country","banner"],char:"🇰🇲",fitzpatrick_scale:false,category:"flags"},congo_brazzaville:{keywords:["congo","flag","nation","country","banner"],char:"🇨🇬",fitzpatrick_scale:false,category:"flags"},congo_kinshasa:{keywords:["congo","democratic","republic","flag","nation","country","banner"],char:"🇨🇩",fitzpatrick_scale:false,category:"flags"},cook_islands:{keywords:["cook","islands","flag","nation","country","banner"],char:"🇨🇰",fitzpatrick_scale:false,category:"flags"},costa_rica:{keywords:["costa","rica","flag","nation","country","banner"],char:"🇨🇷",fitzpatrick_scale:false,category:"flags"},croatia:{keywords:["hr","flag","nation","country","banner"],char:"🇭🇷",fitzpatrick_scale:false,category:"flags"},cuba:{keywords:["cu","flag","nation","country","banner"],char:"🇨🇺",fitzpatrick_scale:false,category:"flags"},curacao:{keywords:["curaçao","flag","nation","country","banner"],char:"🇨🇼",fitzpatrick_scale:false,category:"flags"},cyprus:{keywords:["cy","flag","nation","country","banner"],char:"🇨🇾",fitzpatrick_scale:false,category:"flags"},czech_republic:{keywords:["cz","flag","nation","country","banner"],char:"🇨🇿",fitzpatrick_scale:false,category:"flags"},denmark:{keywords:["dk","flag","nation","country","banner"],char:"🇩🇰",fitzpatrick_scale:false,category:"flags"},djibouti:{keywords:["dj","flag","nation","country","banner"],char:"🇩🇯",fitzpatrick_scale:false,category:"flags"},dominica:{keywords:["dm","flag","nation","country","banner"],char:"🇩🇲",fitzpatrick_scale:false,category:"flags"},dominican_republic:{keywords:["dominican","republic","flag","nation","country","banner"],char:"🇩🇴",fitzpatrick_scale:false,category:"flags"},ecuador:{keywords:["ec","flag","nation","country","banner"],char:"🇪🇨",fitzpatrick_scale:false,category:"flags"},egypt:{keywords:["eg","flag","nation","country","banner"],char:"🇪🇬",fitzpatrick_scale:false,category:"flags"},el_salvador:{keywords:["el","salvador","flag","nation","country","banner"],char:"🇸🇻",fitzpatrick_scale:false,category:"flags"},equatorial_guinea:{keywords:["equatorial","gn","flag","nation","country","banner"],char:"🇬🇶",fitzpatrick_scale:false,category:"flags"},eritrea:{keywords:["er","flag","nation","country","banner"],char:"🇪🇷",fitzpatrick_scale:false,category:"flags"},estonia:{keywords:["ee","flag","nation","country","banner"],char:"🇪🇪",fitzpatrick_scale:false,category:"flags"},ethiopia:{keywords:["et","flag","nation","country","banner"],char:"🇪🇹",fitzpatrick_scale:false,category:"flags"},eu:{keywords:["european","union","flag","banner"],char:"🇪🇺",fitzpatrick_scale:false,category:"flags"},falkland_islands:{keywords:["falkland","islands","malvinas","flag","nation","country","banner"],char:"🇫🇰",fitzpatrick_scale:false,category:"flags"},faroe_islands:{keywords:["faroe","islands","flag","nation","country","banner"],char:"🇫🇴",fitzpatrick_scale:false,category:"flags"},fiji:{keywords:["fj","flag","nation","country","banner"],char:"🇫🇯",fitzpatrick_scale:false,category:"flags"},finland:{keywords:["fi","flag","nation","country","banner"],char:"🇫🇮",fitzpatrick_scale:false,category:"flags"},fr:{keywords:["banner","flag","nation","france","french","country"],char:"🇫🇷",fitzpatrick_scale:false,category:"flags"},french_guiana:{keywords:["french","guiana","flag","nation","country","banner"],char:"🇬🇫",fitzpatrick_scale:false,category:"flags"},french_polynesia:{keywords:["french","polynesia","flag","nation","country","banner"],char:"🇵🇫",fitzpatrick_scale:false,category:"flags"},french_southern_territories:{keywords:["french","southern","territories","flag","nation","country","banner"],char:"🇹🇫",fitzpatrick_scale:false,category:"flags"},gabon:{keywords:["ga","flag","nation","country","banner"],char:"🇬🇦",fitzpatrick_scale:false,category:"flags"},gambia:{keywords:["gm","flag","nation","country","banner"],char:"🇬🇲",fitzpatrick_scale:false,category:"flags"},georgia:{keywords:["ge","flag","nation","country","banner"],char:"🇬🇪",fitzpatrick_scale:false,category:"flags"},de:{keywords:["german","nation","flag","country","banner"],char:"🇩🇪",fitzpatrick_scale:false,category:"flags"},ghana:{keywords:["gh","flag","nation","country","banner"],char:"🇬🇭",fitzpatrick_scale:false,category:"flags"},gibraltar:{keywords:["gi","flag","nation","country","banner"],char:"🇬🇮",fitzpatrick_scale:false,category:"flags"},greece:{keywords:["gr","flag","nation","country","banner"],char:"🇬🇷",fitzpatrick_scale:false,category:"flags"},greenland:{keywords:["gl","flag","nation","country","banner"],char:"🇬🇱",fitzpatrick_scale:false,category:"flags"},grenada:{keywords:["gd","flag","nation","country","banner"],char:"🇬🇩",fitzpatrick_scale:false,category:"flags"},guadeloupe:{keywords:["gp","flag","nation","country","banner"],char:"🇬🇵",fitzpatrick_scale:false,category:"flags"},guam:{keywords:["gu","flag","nation","country","banner"],char:"🇬🇺",fitzpatrick_scale:false,category:"flags"},guatemala:{keywords:["gt","flag","nation","country","banner"],char:"🇬🇹",fitzpatrick_scale:false,category:"flags"},guernsey:{keywords:["gg","flag","nation","country","banner"],char:"🇬🇬",fitzpatrick_scale:false,category:"flags"},guinea:{keywords:["gn","flag","nation","country","banner"],char:"🇬🇳",fitzpatrick_scale:false,category:"flags"},guinea_bissau:{keywords:["gw","bissau","flag","nation","country","banner"],char:"🇬🇼",fitzpatrick_scale:false,category:"flags"},guyana:{keywords:["gy","flag","nation","country","banner"],char:"🇬🇾",fitzpatrick_scale:false,category:"flags"},haiti:{keywords:["ht","flag","nation","country","banner"],char:"🇭🇹",fitzpatrick_scale:false,category:"flags"},honduras:{keywords:["hn","flag","nation","country","banner"],char:"🇭🇳",fitzpatrick_scale:false,category:"flags"},hong_kong:{keywords:["hong","kong","flag","nation","country","banner"],char:"🇭🇰",fitzpatrick_scale:false,category:"flags"},hungary:{keywords:["hu","flag","nation","country","banner"],char:"🇭🇺",fitzpatrick_scale:false,category:"flags"},iceland:{keywords:["is","flag","nation","country","banner"],char:"🇮🇸",fitzpatrick_scale:false,category:"flags"},india:{keywords:["in","flag","nation","country","banner"],char:"🇮🇳",fitzpatrick_scale:false,category:"flags"},indonesia:{keywords:["flag","nation","country","banner"],char:"🇮🇩",fitzpatrick_scale:false,category:"flags"},iran:{keywords:["iran,","islamic","republic","flag","nation","country","banner"],char:"🇮🇷",fitzpatrick_scale:false,category:"flags"},iraq:{keywords:["iq","flag","nation","country","banner"],char:"🇮🇶",fitzpatrick_scale:false,category:"flags"},ireland:{keywords:["ie","flag","nation","country","banner"],char:"🇮🇪",fitzpatrick_scale:false,category:"flags"},isle_of_man:{keywords:["isle","man","flag","nation","country","banner"],char:"🇮🇲",fitzpatrick_scale:false,category:"flags"},israel:{keywords:["il","flag","nation","country","banner"],char:"🇮🇱",fitzpatrick_scale:false,category:"flags"},it:{keywords:["italy","flag","nation","country","banner"],char:"🇮🇹",fitzpatrick_scale:false,category:"flags"},cote_divoire:{keywords:["ivory","coast","flag","nation","country","banner"],char:"🇨🇮",fitzpatrick_scale:false,category:"flags"},jamaica:{keywords:["jm","flag","nation","country","banner"],char:"🇯🇲",fitzpatrick_scale:false,category:"flags"},jp:{keywords:["japanese","nation","flag","country","banner"],char:"🇯🇵",fitzpatrick_scale:false,category:"flags"},jersey:{keywords:["je","flag","nation","country","banner"],char:"🇯🇪",fitzpatrick_scale:false,category:"flags"},jordan:{keywords:["jo","flag","nation","country","banner"],char:"🇯🇴",fitzpatrick_scale:false,category:"flags"},kazakhstan:{keywords:["kz","flag","nation","country","banner"],char:"🇰🇿",fitzpatrick_scale:false,category:"flags"},kenya:{keywords:["ke","flag","nation","country","banner"],char:"🇰🇪",fitzpatrick_scale:false,category:"flags"},kiribati:{keywords:["ki","flag","nation","country","banner"],char:"🇰🇮",fitzpatrick_scale:false,category:"flags"},kosovo:{keywords:["xk","flag","nation","country","banner"],char:"🇽🇰",fitzpatrick_scale:false,category:"flags"},kuwait:{keywords:["kw","flag","nation","country","banner"],char:"🇰🇼",fitzpatrick_scale:false,category:"flags"},kyrgyzstan:{keywords:["kg","flag","nation","country","banner"],char:"🇰🇬",fitzpatrick_scale:false,category:"flags"},laos:{keywords:["lao","democratic","republic","flag","nation","country","banner"],char:"🇱🇦",fitzpatrick_scale:false,category:"flags"},latvia:{keywords:["lv","flag","nation","country","banner"],char:"🇱🇻",fitzpatrick_scale:false,category:"flags"},lebanon:{keywords:["lb","flag","nation","country","banner"],char:"🇱🇧",fitzpatrick_scale:false,category:"flags"},lesotho:{keywords:["ls","flag","nation","country","banner"],char:"🇱🇸",fitzpatrick_scale:false,category:"flags"},liberia:{keywords:["lr","flag","nation","country","banner"],char:"🇱🇷",fitzpatrick_scale:false,category:"flags"},libya:{keywords:["ly","flag","nation","country","banner"],char:"🇱🇾",fitzpatrick_scale:false,category:"flags"},liechtenstein:{keywords:["li","flag","nation","country","banner"],char:"🇱🇮",fitzpatrick_scale:false,category:"flags"},lithuania:{keywords:["lt","flag","nation","country","banner"],char:"🇱🇹",fitzpatrick_scale:false,category:"flags"},luxembourg:{keywords:["lu","flag","nation","country","banner"],char:"🇱🇺",fitzpatrick_scale:false,category:"flags"},macau:{keywords:["macao","flag","nation","country","banner"],char:"🇲🇴",fitzpatrick_scale:false,category:"flags"},macedonia:{keywords:["macedonia,","flag","nation","country","banner"],char:"🇲🇰",fitzpatrick_scale:false,category:"flags"},madagascar:{keywords:["mg","flag","nation","country","banner"],char:"🇲🇬",fitzpatrick_scale:false,category:"flags"},malawi:{keywords:["mw","flag","nation","country","banner"],char:"🇲🇼",fitzpatrick_scale:false,category:"flags"},malaysia:{keywords:["my","flag","nation","country","banner"],char:"🇲🇾",fitzpatrick_scale:false,category:"flags"},maldives:{keywords:["mv","flag","nation","country","banner"],char:"🇲🇻",fitzpatrick_scale:false,category:"flags"},mali:{keywords:["ml","flag","nation","country","banner"],char:"🇲🇱",fitzpatrick_scale:false,category:"flags"},malta:{keywords:["mt","flag","nation","country","banner"],char:"🇲🇹",fitzpatrick_scale:false,category:"flags"},marshall_islands:{keywords:["marshall","islands","flag","nation","country","banner"],char:"🇲🇭",fitzpatrick_scale:false,category:"flags"},martinique:{keywords:["mq","flag","nation","country","banner"],char:"🇲🇶",fitzpatrick_scale:false,category:"flags"},mauritania:{keywords:["mr","flag","nation","country","banner"],char:"🇲🇷",fitzpatrick_scale:false,category:"flags"},mauritius:{keywords:["mu","flag","nation","country","banner"],char:"🇲🇺",fitzpatrick_scale:false,category:"flags"},mayotte:{keywords:["yt","flag","nation","country","banner"],char:"🇾🇹",fitzpatrick_scale:false,category:"flags"},mexico:{keywords:["mx","flag","nation","country","banner"],char:"🇲🇽",fitzpatrick_scale:false,category:"flags"},micronesia:{keywords:["micronesia,","federated","states","flag","nation","country","banner"],char:"🇫🇲",fitzpatrick_scale:false,category:"flags"},moldova:{keywords:["moldova,","republic","flag","nation","country","banner"],char:"🇲🇩",fitzpatrick_scale:false,category:"flags"},monaco:{keywords:["mc","flag","nation","country","banner"],char:"🇲🇨",fitzpatrick_scale:false,category:"flags"},mongolia:{keywords:["mn","flag","nation","country","banner"],char:"🇲🇳",fitzpatrick_scale:false,category:"flags"},montenegro:{keywords:["me","flag","nation","country","banner"],char:"🇲🇪",fitzpatrick_scale:false,category:"flags"},montserrat:{keywords:["ms","flag","nation","country","banner"],char:"🇲🇸",fitzpatrick_scale:false,category:"flags"},morocco:{keywords:["ma","flag","nation","country","banner"],char:"🇲🇦",fitzpatrick_scale:false,category:"flags"},mozambique:{keywords:["mz","flag","nation","country","banner"],char:"🇲🇿",fitzpatrick_scale:false,category:"flags"},myanmar:{keywords:["mm","flag","nation","country","banner"],char:"🇲🇲",fitzpatrick_scale:false,category:"flags"},namibia:{keywords:["na","flag","nation","country","banner"],char:"🇳🇦",fitzpatrick_scale:false,category:"flags"},nauru:{keywords:["nr","flag","nation","country","banner"],char:"🇳🇷",fitzpatrick_scale:false,category:"flags"},nepal:{keywords:["np","flag","nation","country","banner"],char:"🇳🇵",fitzpatrick_scale:false,category:"flags"},netherlands:{keywords:["nl","flag","nation","country","banner"],char:"🇳🇱",fitzpatrick_scale:false,category:"flags"},new_caledonia:{keywords:["new","caledonia","flag","nation","country","banner"],char:"🇳🇨",fitzpatrick_scale:false,category:"flags"},new_zealand:{keywords:["new","zealand","flag","nation","country","banner"],char:"🇳🇿",fitzpatrick_scale:false,category:"flags"},nicaragua:{keywords:["ni","flag","nation","country","banner"],char:"🇳🇮",fitzpatrick_scale:false,category:"flags"},niger:{keywords:["ne","flag","nation","country","banner"],char:"🇳🇪",fitzpatrick_scale:false,category:"flags"},nigeria:{keywords:["flag","nation","country","banner"],char:"🇳🇬",fitzpatrick_scale:false,category:"flags"},niue:{keywords:["nu","flag","nation","country","banner"],char:"🇳🇺",fitzpatrick_scale:false,category:"flags"},norfolk_island:{keywords:["norfolk","island","flag","nation","country","banner"],char:"🇳🇫",fitzpatrick_scale:false,category:"flags"},northern_mariana_islands:{keywords:["northern","mariana","islands","flag","nation","country","banner"],char:"🇲🇵",fitzpatrick_scale:false,category:"flags"},north_korea:{keywords:["north","korea","nation","flag","country","banner"],char:"🇰🇵",fitzpatrick_scale:false,category:"flags"},norway:{keywords:["no","flag","nation","country","banner"],char:"🇳🇴",fitzpatrick_scale:false,category:"flags"},oman:{keywords:["om_symbol","flag","nation","country","banner"],char:"🇴🇲",fitzpatrick_scale:false,category:"flags"},pakistan:{keywords:["pk","flag","nation","country","banner"],char:"🇵🇰",fitzpatrick_scale:false,category:"flags"},palau:{keywords:["pw","flag","nation","country","banner"],char:"🇵🇼",fitzpatrick_scale:false,category:"flags"},palestinian_territories:{keywords:["palestine","palestinian","territories","flag","nation","country","banner"],char:"🇵🇸",fitzpatrick_scale:false,category:"flags"},panama:{keywords:["pa","flag","nation","country","banner"],char:"🇵🇦",fitzpatrick_scale:false,category:"flags"},papua_new_guinea:{keywords:["papua","new","guinea","flag","nation","country","banner"],char:"🇵🇬",fitzpatrick_scale:false,category:"flags"},paraguay:{keywords:["py","flag","nation","country","banner"],char:"🇵🇾",fitzpatrick_scale:false,category:"flags"},peru:{keywords:["pe","flag","nation","country","banner"],char:"🇵🇪",fitzpatrick_scale:false,category:"flags"},philippines:{keywords:["ph","flag","nation","country","banner"],char:"🇵🇭",fitzpatrick_scale:false,category:"flags"},pitcairn_islands:{keywords:["pitcairn","flag","nation","country","banner"],char:"🇵🇳",fitzpatrick_scale:false,category:"flags"},poland:{keywords:["pl","flag","nation","country","banner"],char:"🇵🇱",fitzpatrick_scale:false,category:"flags"},portugal:{keywords:["pt","flag","nation","country","banner"],char:"🇵🇹",fitzpatrick_scale:false,category:"flags"},puerto_rico:{keywords:["puerto","rico","flag","nation","country","banner"],char:"🇵🇷",fitzpatrick_scale:false,category:"flags"},qatar:{keywords:["qa","flag","nation","country","banner"],char:"🇶🇦",fitzpatrick_scale:false,category:"flags"},reunion:{keywords:["réunion","flag","nation","country","banner"],char:"🇷🇪",fitzpatrick_scale:false,category:"flags"},romania:{keywords:["ro","flag","nation","country","banner"],char:"🇷🇴",fitzpatrick_scale:false,category:"flags"},ru:{keywords:["russian","federation","flag","nation","country","banner"],char:"🇷🇺",fitzpatrick_scale:false,category:"flags"},rwanda:{keywords:["rw","flag","nation","country","banner"],char:"🇷🇼",fitzpatrick_scale:false,category:"flags"},st_barthelemy:{keywords:["saint","barthélemy","flag","nation","country","banner"],char:"🇧🇱",fitzpatrick_scale:false,category:"flags"},st_helena:{keywords:["saint","helena","ascension","tristan","cunha","flag","nation","country","banner"],char:"🇸🇭",fitzpatrick_scale:false,category:"flags"},st_kitts_nevis:{keywords:["saint","kitts","nevis","flag","nation","country","banner"],char:"🇰🇳",fitzpatrick_scale:false,category:"flags"},st_lucia:{keywords:["saint","lucia","flag","nation","country","banner"],char:"🇱🇨",fitzpatrick_scale:false,category:"flags"},st_pierre_miquelon:{keywords:["saint","pierre","miquelon","flag","nation","country","banner"],char:"🇵🇲",fitzpatrick_scale:false,category:"flags"},st_vincent_grenadines:{keywords:["saint","vincent","grenadines","flag","nation","country","banner"],char:"🇻🇨",fitzpatrick_scale:false,category:"flags"},samoa:{keywords:["ws","flag","nation","country","banner"],char:"🇼🇸",fitzpatrick_scale:false,category:"flags"},san_marino:{keywords:["san","marino","flag","nation","country","banner"],char:"🇸🇲",fitzpatrick_scale:false,category:"flags"},sao_tome_principe:{keywords:["sao","tome","principe","flag","nation","country","banner"],char:"🇸🇹",fitzpatrick_scale:false,category:"flags"},saudi_arabia:{keywords:["flag","nation","country","banner"],char:"🇸🇦",fitzpatrick_scale:false,category:"flags"},senegal:{keywords:["sn","flag","nation","country","banner"],char:"🇸🇳",fitzpatrick_scale:false,category:"flags"},serbia:{keywords:["rs","flag","nation","country","banner"],char:"🇷🇸",fitzpatrick_scale:false,category:"flags"},seychelles:{keywords:["sc","flag","nation","country","banner"],char:"🇸🇨",fitzpatrick_scale:false,category:"flags"},sierra_leone:{keywords:["sierra","leone","flag","nation","country","banner"],char:"🇸🇱",fitzpatrick_scale:false,category:"flags"},singapore:{keywords:["sg","flag","nation","country","banner"],char:"🇸🇬",fitzpatrick_scale:false,category:"flags"},sint_maarten:{keywords:["sint","maarten","dutch","flag","nation","country","banner"],char:"🇸🇽",fitzpatrick_scale:false,category:"flags"},slovakia:{keywords:["sk","flag","nation","country","banner"],char:"🇸🇰",fitzpatrick_scale:false,category:"flags"},slovenia:{keywords:["si","flag","nation","country","banner"],char:"🇸🇮",fitzpatrick_scale:false,category:"flags"},solomon_islands:{keywords:["solomon","islands","flag","nation","country","banner"],char:"🇸🇧",fitzpatrick_scale:false,category:"flags"},somalia:{keywords:["so","flag","nation","country","banner"],char:"🇸🇴",fitzpatrick_scale:false,category:"flags"},south_africa:{keywords:["south","africa","flag","nation","country","banner"],char:"🇿🇦",fitzpatrick_scale:false,category:"flags"},south_georgia_south_sandwich_islands:{keywords:["south","georgia","sandwich","islands","flag","nation","country","banner"],char:"🇬🇸",fitzpatrick_scale:false,category:"flags"},kr:{keywords:["south","korea","nation","flag","country","banner"],char:"🇰🇷",fitzpatrick_scale:false,category:"flags"},south_sudan:{keywords:["south","sd","flag","nation","country","banner"],char:"🇸🇸",fitzpatrick_scale:false,category:"flags"},es:{keywords:["spain","flag","nation","country","banner"],char:"🇪🇸",fitzpatrick_scale:false,category:"flags"},sri_lanka:{keywords:["sri","lanka","flag","nation","country","banner"],char:"🇱🇰",fitzpatrick_scale:false,category:"flags"},sudan:{keywords:["sd","flag","nation","country","banner"],char:"🇸🇩",fitzpatrick_scale:false,category:"flags"},suriname:{keywords:["sr","flag","nation","country","banner"],char:"🇸🇷",fitzpatrick_scale:false,category:"flags"},swaziland:{keywords:["sz","flag","nation","country","banner"],char:"🇸🇿",fitzpatrick_scale:false,category:"flags"},sweden:{keywords:["se","flag","nation","country","banner"],char:"🇸🇪",fitzpatrick_scale:false,category:"flags"},switzerland:{keywords:["ch","flag","nation","country","banner"],char:"🇨🇭",fitzpatrick_scale:false,category:"flags"},syria:{keywords:["syrian","arab","republic","flag","nation","country","banner"],char:"🇸🇾",fitzpatrick_scale:false,category:"flags"},taiwan:{keywords:["tw","flag","nation","country","banner"],char:"🇹🇼",fitzpatrick_scale:false,category:"flags"},tajikistan:{keywords:["tj","flag","nation","country","banner"],char:"🇹🇯",fitzpatrick_scale:false,category:"flags"},tanzania:{keywords:["tanzania,","united","republic","flag","nation","country","banner"],char:"🇹🇿",fitzpatrick_scale:false,category:"flags"},thailand:{keywords:["th","flag","nation","country","banner"],char:"🇹🇭",fitzpatrick_scale:false,category:"flags"},timor_leste:{keywords:["timor","leste","flag","nation","country","banner"],char:"🇹🇱",fitzpatrick_scale:false,category:"flags"},togo:{keywords:["tg","flag","nation","country","banner"],char:"🇹🇬",fitzpatrick_scale:false,category:"flags"},tokelau:{keywords:["tk","flag","nation","country","banner"],char:"🇹🇰",fitzpatrick_scale:false,category:"flags"},tonga:{keywords:["to","flag","nation","country","banner"],char:"🇹🇴",fitzpatrick_scale:false,category:"flags"},trinidad_tobago:{keywords:["trinidad","tobago","flag","nation","country","banner"],char:"🇹🇹",fitzpatrick_scale:false,category:"flags"},tunisia:{keywords:["tn","flag","nation","country","banner"],char:"🇹🇳",fitzpatrick_scale:false,category:"flags"},tr:{keywords:["turkey","flag","nation","country","banner"],char:"🇹🇷",fitzpatrick_scale:false,category:"flags"},turkmenistan:{keywords:["flag","nation","country","banner"],char:"🇹🇲",fitzpatrick_scale:false,category:"flags"},turks_caicos_islands:{keywords:["turks","caicos","islands","flag","nation","country","banner"],char:"🇹🇨",fitzpatrick_scale:false,category:"flags"},tuvalu:{keywords:["flag","nation","country","banner"],char:"🇹🇻",fitzpatrick_scale:false,category:"flags"},uganda:{keywords:["ug","flag","nation","country","banner"],char:"🇺🇬",fitzpatrick_scale:false,category:"flags"},ukraine:{keywords:["ua","flag","nation","country","banner"],char:"🇺🇦",fitzpatrick_scale:false,category:"flags"},united_arab_emirates:{keywords:["united","arab","emirates","flag","nation","country","banner"],char:"🇦🇪",fitzpatrick_scale:false,category:"flags"},uk:{keywords:["united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","UK","english","england","union jack"],char:"🇬🇧",fitzpatrick_scale:false,category:"flags"},england:{keywords:["flag","english"],char:"🏴󠁧󠁢󠁥󠁮󠁧󠁿",fitzpatrick_scale:false,category:"flags"},scotland:{keywords:["flag","scottish"],char:"🏴󠁧󠁢󠁳󠁣󠁴󠁿",fitzpatrick_scale:false,category:"flags"},wales:{keywords:["flag","welsh"],char:"🏴󠁧󠁢󠁷󠁬󠁳󠁿",fitzpatrick_scale:false,category:"flags"},us:{keywords:["united","states","america","flag","nation","country","banner"],char:"🇺🇸",fitzpatrick_scale:false,category:"flags"},us_virgin_islands:{keywords:["virgin","islands","us","flag","nation","country","banner"],char:"🇻🇮",fitzpatrick_scale:false,category:"flags"},uruguay:{keywords:["uy","flag","nation","country","banner"],char:"🇺🇾",fitzpatrick_scale:false,category:"flags"},uzbekistan:{keywords:["uz","flag","nation","country","banner"],char:"🇺🇿",fitzpatrick_scale:false,category:"flags"},vanuatu:{keywords:["vu","flag","nation","country","banner"],char:"🇻🇺",fitzpatrick_scale:false,category:"flags"},vatican_city:{keywords:["vatican","city","flag","nation","country","banner"],char:"🇻🇦",fitzpatrick_scale:false,category:"flags"},venezuela:{keywords:["ve","bolivarian","republic","flag","nation","country","banner"],char:"🇻🇪",fitzpatrick_scale:false,category:"flags"},vietnam:{keywords:["viet","nam","flag","nation","country","banner"],char:"🇻🇳",fitzpatrick_scale:false,category:"flags"},wallis_futuna:{keywords:["wallis","futuna","flag","nation","country","banner"],char:"🇼🇫",fitzpatrick_scale:false,category:"flags"},western_sahara:{keywords:["western","sahara","flag","nation","country","banner"],char:"🇪🇭",fitzpatrick_scale:false,category:"flags"},yemen:{keywords:["ye","flag","nation","country","banner"],char:"🇾🇪",fitzpatrick_scale:false,category:"flags"},zambia:{keywords:["zm","flag","nation","country","banner"],char:"🇿🇲",fitzpatrick_scale:false,category:"flags"},zimbabwe:{keywords:["zw","flag","nation","country","banner"],char:"🇿🇼",fitzpatrick_scale:false,category:"flags"},united_nations:{keywords:["un","flag","banner"],char:"🇺🇳",fitzpatrick_scale:false,category:"flags"},pirate_flag:{keywords:["skull","crossbones","flag","banner"],char:"🏴‍☠️",fitzpatrick_scale:false,category:"flags"}}); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/emoticons/js/emojis.min.js b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojis.min.js new file mode 100644 index 0000000..5a1c491 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/emoticons/js/emojis.min.js @@ -0,0 +1,2 @@ +// Source: npm package: emojilib, file:emojis.json +window.tinymce.Resource.add("tinymce.plugins.emoticons",{grinning:{keywords:["face","smile","happy","joy",":D","grin"],char:"\u{1f600}",fitzpatrick_scale:!1,category:"people"},grimacing:{keywords:["face","grimace","teeth"],char:"\u{1f62c}",fitzpatrick_scale:!1,category:"people"},grin:{keywords:["face","happy","smile","joy","kawaii"],char:"\u{1f601}",fitzpatrick_scale:!1,category:"people"},joy:{keywords:["face","cry","tears","weep","happy","happytears","haha"],char:"\u{1f602}",fitzpatrick_scale:!1,category:"people"},rofl:{keywords:["face","rolling","floor","laughing","lol","haha"],char:"\u{1f923}",fitzpatrick_scale:!1,category:"people"},partying:{keywords:["face","celebration","woohoo"],char:"\u{1f973}",fitzpatrick_scale:!1,category:"people"},smiley:{keywords:["face","happy","joy","haha",":D",":)","smile","funny"],char:"\u{1f603}",fitzpatrick_scale:!1,category:"people"},smile:{keywords:["face","happy","joy","funny","haha","laugh","like",":D",":)"],char:"\u{1f604}",fitzpatrick_scale:!1,category:"people"},sweat_smile:{keywords:["face","hot","happy","laugh","sweat","smile","relief"],char:"\u{1f605}",fitzpatrick_scale:!1,category:"people"},laughing:{keywords:["happy","joy","lol","satisfied","haha","face","glad","XD","laugh"],char:"\u{1f606}",fitzpatrick_scale:!1,category:"people"},innocent:{keywords:["face","angel","heaven","halo"],char:"\u{1f607}",fitzpatrick_scale:!1,category:"people"},wink:{keywords:["face","happy","mischievous","secret",";)","smile","eye"],char:"\u{1f609}",fitzpatrick_scale:!1,category:"people"},blush:{keywords:["face","smile","happy","flushed","crush","embarrassed","shy","joy"],char:"\u{1f60a}",fitzpatrick_scale:!1,category:"people"},slightly_smiling_face:{keywords:["face","smile"],char:"\u{1f642}",fitzpatrick_scale:!1,category:"people"},upside_down_face:{keywords:["face","flipped","silly","smile"],char:"\u{1f643}",fitzpatrick_scale:!1,category:"people"},relaxed:{keywords:["face","blush","massage","happiness"],char:"\u263a\ufe0f",fitzpatrick_scale:!1,category:"people"},yum:{keywords:["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"],char:"\u{1f60b}",fitzpatrick_scale:!1,category:"people"},relieved:{keywords:["face","relaxed","phew","massage","happiness"],char:"\u{1f60c}",fitzpatrick_scale:!1,category:"people"},heart_eyes:{keywords:["face","love","like","affection","valentines","infatuation","crush","heart"],char:"\u{1f60d}",fitzpatrick_scale:!1,category:"people"},smiling_face_with_three_hearts:{keywords:["face","love","like","affection","valentines","infatuation","crush","hearts","adore"],char:"\u{1f970}",fitzpatrick_scale:!1,category:"people"},kissing_heart:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"\u{1f618}",fitzpatrick_scale:!1,category:"people"},kissing:{keywords:["love","like","face","3","valentines","infatuation","kiss"],char:"\u{1f617}",fitzpatrick_scale:!1,category:"people"},kissing_smiling_eyes:{keywords:["face","affection","valentines","infatuation","kiss"],char:"\u{1f619}",fitzpatrick_scale:!1,category:"people"},kissing_closed_eyes:{keywords:["face","love","like","affection","valentines","infatuation","kiss"],char:"\u{1f61a}",fitzpatrick_scale:!1,category:"people"},stuck_out_tongue_winking_eye:{keywords:["face","prank","childish","playful","mischievous","smile","wink","tongue"],char:"\u{1f61c}",fitzpatrick_scale:!1,category:"people"},zany:{keywords:["face","goofy","crazy"],char:"\u{1f92a}",fitzpatrick_scale:!1,category:"people"},raised_eyebrow:{keywords:["face","distrust","scepticism","disapproval","disbelief","surprise"],char:"\u{1f928}",fitzpatrick_scale:!1,category:"people"},monocle:{keywords:["face","stuffy","wealthy"],char:"\u{1f9d0}",fitzpatrick_scale:!1,category:"people"},stuck_out_tongue_closed_eyes:{keywords:["face","prank","playful","mischievous","smile","tongue"],char:"\u{1f61d}",fitzpatrick_scale:!1,category:"people"},stuck_out_tongue:{keywords:["face","prank","childish","playful","mischievous","smile","tongue"],char:"\u{1f61b}",fitzpatrick_scale:!1,category:"people"},money_mouth_face:{keywords:["face","rich","dollar","money"],char:"\u{1f911}",fitzpatrick_scale:!1,category:"people"},nerd_face:{keywords:["face","nerdy","geek","dork"],char:"\u{1f913}",fitzpatrick_scale:!1,category:"people"},sunglasses:{keywords:["face","cool","smile","summer","beach","sunglass"],char:"\u{1f60e}",fitzpatrick_scale:!1,category:"people"},star_struck:{keywords:["face","smile","starry","eyes","grinning"],char:"\u{1f929}",fitzpatrick_scale:!1,category:"people"},clown_face:{keywords:["face"],char:"\u{1f921}",fitzpatrick_scale:!1,category:"people"},cowboy_hat_face:{keywords:["face","cowgirl","hat"],char:"\u{1f920}",fitzpatrick_scale:!1,category:"people"},hugs:{keywords:["face","smile","hug"],char:"\u{1f917}",fitzpatrick_scale:!1,category:"people"},smirk:{keywords:["face","smile","mean","prank","smug","sarcasm"],char:"\u{1f60f}",fitzpatrick_scale:!1,category:"people"},no_mouth:{keywords:["face","hellokitty"],char:"\u{1f636}",fitzpatrick_scale:!1,category:"people"},neutral_face:{keywords:["indifference","meh",":|","neutral"],char:"\u{1f610}",fitzpatrick_scale:!1,category:"people"},expressionless:{keywords:["face","indifferent","-_-","meh","deadpan"],char:"\u{1f611}",fitzpatrick_scale:!1,category:"people"},unamused:{keywords:["indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"],char:"\u{1f612}",fitzpatrick_scale:!1,category:"people"},roll_eyes:{keywords:["face","eyeroll","frustrated"],char:"\u{1f644}",fitzpatrick_scale:!1,category:"people"},thinking:{keywords:["face","hmmm","think","consider"],char:"\u{1f914}",fitzpatrick_scale:!1,category:"people"},lying_face:{keywords:["face","lie","pinocchio"],char:"\u{1f925}",fitzpatrick_scale:!1,category:"people"},hand_over_mouth:{keywords:["face","whoops","shock","surprise"],char:"\u{1f92d}",fitzpatrick_scale:!1,category:"people"},shushing:{keywords:["face","quiet","shhh"],char:"\u{1f92b}",fitzpatrick_scale:!1,category:"people"},symbols_over_mouth:{keywords:["face","swearing","cursing","cussing","profanity","expletive"],char:"\u{1f92c}",fitzpatrick_scale:!1,category:"people"},exploding_head:{keywords:["face","shocked","mind","blown"],char:"\u{1f92f}",fitzpatrick_scale:!1,category:"people"},flushed:{keywords:["face","blush","shy","flattered"],char:"\u{1f633}",fitzpatrick_scale:!1,category:"people"},disappointed:{keywords:["face","sad","upset","depressed",":("],char:"\u{1f61e}",fitzpatrick_scale:!1,category:"people"},worried:{keywords:["face","concern","nervous",":("],char:"\u{1f61f}",fitzpatrick_scale:!1,category:"people"},angry:{keywords:["mad","face","annoyed","frustrated"],char:"\u{1f620}",fitzpatrick_scale:!1,category:"people"},rage:{keywords:["angry","mad","hate","despise"],char:"\u{1f621}",fitzpatrick_scale:!1,category:"people"},pensive:{keywords:["face","sad","depressed","upset"],char:"\u{1f614}",fitzpatrick_scale:!1,category:"people"},confused:{keywords:["face","indifference","huh","weird","hmmm",":/"],char:"\u{1f615}",fitzpatrick_scale:!1,category:"people"},slightly_frowning_face:{keywords:["face","frowning","disappointed","sad","upset"],char:"\u{1f641}",fitzpatrick_scale:!1,category:"people"},frowning_face:{keywords:["face","sad","upset","frown"],char:"\u2639",fitzpatrick_scale:!1,category:"people"},persevere:{keywords:["face","sick","no","upset","oops"],char:"\u{1f623}",fitzpatrick_scale:!1,category:"people"},confounded:{keywords:["face","confused","sick","unwell","oops",":S"],char:"\u{1f616}",fitzpatrick_scale:!1,category:"people"},tired_face:{keywords:["sick","whine","upset","frustrated"],char:"\u{1f62b}",fitzpatrick_scale:!1,category:"people"},weary:{keywords:["face","tired","sleepy","sad","frustrated","upset"],char:"\u{1f629}",fitzpatrick_scale:!1,category:"people"},pleading:{keywords:["face","begging","mercy"],char:"\u{1f97a}",fitzpatrick_scale:!1,category:"people"},triumph:{keywords:["face","gas","phew","proud","pride"],char:"\u{1f624}",fitzpatrick_scale:!1,category:"people"},open_mouth:{keywords:["face","surprise","impressed","wow","whoa",":O"],char:"\u{1f62e}",fitzpatrick_scale:!1,category:"people"},scream:{keywords:["face","munch","scared","omg"],char:"\u{1f631}",fitzpatrick_scale:!1,category:"people"},fearful:{keywords:["face","scared","terrified","nervous","oops","huh"],char:"\u{1f628}",fitzpatrick_scale:!1,category:"people"},cold_sweat:{keywords:["face","nervous","sweat"],char:"\u{1f630}",fitzpatrick_scale:!1,category:"people"},hushed:{keywords:["face","woo","shh"],char:"\u{1f62f}",fitzpatrick_scale:!1,category:"people"},frowning:{keywords:["face","aw","what"],char:"\u{1f626}",fitzpatrick_scale:!1,category:"people"},anguished:{keywords:["face","stunned","nervous"],char:"\u{1f627}",fitzpatrick_scale:!1,category:"people"},cry:{keywords:["face","tears","sad","depressed","upset",":'("],char:"\u{1f622}",fitzpatrick_scale:!1,category:"people"},disappointed_relieved:{keywords:["face","phew","sweat","nervous"],char:"\u{1f625}",fitzpatrick_scale:!1,category:"people"},drooling_face:{keywords:["face"],char:"\u{1f924}",fitzpatrick_scale:!1,category:"people"},sleepy:{keywords:["face","tired","rest","nap"],char:"\u{1f62a}",fitzpatrick_scale:!1,category:"people"},sweat:{keywords:["face","hot","sad","tired","exercise"],char:"\u{1f613}",fitzpatrick_scale:!1,category:"people"},hot:{keywords:["face","feverish","heat","red","sweating"],char:"\u{1f975}",fitzpatrick_scale:!1,category:"people"},cold:{keywords:["face","blue","freezing","frozen","frostbite","icicles"],char:"\u{1f976}",fitzpatrick_scale:!1,category:"people"},sob:{keywords:["face","cry","tears","sad","upset","depressed"],char:"\u{1f62d}",fitzpatrick_scale:!1,category:"people"},dizzy_face:{keywords:["spent","unconscious","xox","dizzy"],char:"\u{1f635}",fitzpatrick_scale:!1,category:"people"},astonished:{keywords:["face","xox","surprised","poisoned"],char:"\u{1f632}",fitzpatrick_scale:!1,category:"people"},zipper_mouth_face:{keywords:["face","sealed","zipper","secret"],char:"\u{1f910}",fitzpatrick_scale:!1,category:"people"},nauseated_face:{keywords:["face","vomit","gross","green","sick","throw up","ill"],char:"\u{1f922}",fitzpatrick_scale:!1,category:"people"},sneezing_face:{keywords:["face","gesundheit","sneeze","sick","allergy"],char:"\u{1f927}",fitzpatrick_scale:!1,category:"people"},vomiting:{keywords:["face","sick"],char:"\u{1f92e}",fitzpatrick_scale:!1,category:"people"},mask:{keywords:["face","sick","ill","disease"],char:"\u{1f637}",fitzpatrick_scale:!1,category:"people"},face_with_thermometer:{keywords:["sick","temperature","thermometer","cold","fever"],char:"\u{1f912}",fitzpatrick_scale:!1,category:"people"},face_with_head_bandage:{keywords:["injured","clumsy","bandage","hurt"],char:"\u{1f915}",fitzpatrick_scale:!1,category:"people"},woozy:{keywords:["face","dizzy","intoxicated","tipsy","wavy"],char:"\u{1f974}",fitzpatrick_scale:!1,category:"people"},sleeping:{keywords:["face","tired","sleepy","night","zzz"],char:"\u{1f634}",fitzpatrick_scale:!1,category:"people"},zzz:{keywords:["sleepy","tired","dream"],char:"\u{1f4a4}",fitzpatrick_scale:!1,category:"people"},poop:{keywords:["hankey","shitface","fail","turd","shit"],char:"\u{1f4a9}",fitzpatrick_scale:!1,category:"people"},smiling_imp:{keywords:["devil","horns"],char:"\u{1f608}",fitzpatrick_scale:!1,category:"people"},imp:{keywords:["devil","angry","horns"],char:"\u{1f47f}",fitzpatrick_scale:!1,category:"people"},japanese_ogre:{keywords:["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"],char:"\u{1f479}",fitzpatrick_scale:!1,category:"people"},japanese_goblin:{keywords:["red","evil","mask","monster","scary","creepy","japanese","goblin"],char:"\u{1f47a}",fitzpatrick_scale:!1,category:"people"},skull:{keywords:["dead","skeleton","creepy","death"],char:"\u{1f480}",fitzpatrick_scale:!1,category:"people"},ghost:{keywords:["halloween","spooky","scary"],char:"\u{1f47b}",fitzpatrick_scale:!1,category:"people"},alien:{keywords:["UFO","paul","weird","outer_space"],char:"\u{1f47d}",fitzpatrick_scale:!1,category:"people"},robot:{keywords:["computer","machine","bot"],char:"\u{1f916}",fitzpatrick_scale:!1,category:"people"},smiley_cat:{keywords:["animal","cats","happy","smile"],char:"\u{1f63a}",fitzpatrick_scale:!1,category:"people"},smile_cat:{keywords:["animal","cats","smile"],char:"\u{1f638}",fitzpatrick_scale:!1,category:"people"},joy_cat:{keywords:["animal","cats","haha","happy","tears"],char:"\u{1f639}",fitzpatrick_scale:!1,category:"people"},heart_eyes_cat:{keywords:["animal","love","like","affection","cats","valentines","heart"],char:"\u{1f63b}",fitzpatrick_scale:!1,category:"people"},smirk_cat:{keywords:["animal","cats","smirk"],char:"\u{1f63c}",fitzpatrick_scale:!1,category:"people"},kissing_cat:{keywords:["animal","cats","kiss"],char:"\u{1f63d}",fitzpatrick_scale:!1,category:"people"},scream_cat:{keywords:["animal","cats","munch","scared","scream"],char:"\u{1f640}",fitzpatrick_scale:!1,category:"people"},crying_cat_face:{keywords:["animal","tears","weep","sad","cats","upset","cry"],char:"\u{1f63f}",fitzpatrick_scale:!1,category:"people"},pouting_cat:{keywords:["animal","cats"],char:"\u{1f63e}",fitzpatrick_scale:!1,category:"people"},palms_up:{keywords:["hands","gesture","cupped","prayer"],char:"\u{1f932}",fitzpatrick_scale:!0,category:"people"},raised_hands:{keywords:["gesture","hooray","yea","celebration","hands"],char:"\u{1f64c}",fitzpatrick_scale:!0,category:"people"},clap:{keywords:["hands","praise","applause","congrats","yay"],char:"\u{1f44f}",fitzpatrick_scale:!0,category:"people"},wave:{keywords:["hands","gesture","goodbye","solong","farewell","hello","hi","palm"],char:"\u{1f44b}",fitzpatrick_scale:!0,category:"people"},call_me_hand:{keywords:["hands","gesture"],char:"\u{1f919}",fitzpatrick_scale:!0,category:"people"},"+1":{keywords:["thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],char:"\u{1f44d}",fitzpatrick_scale:!0,category:"people"},"-1":{keywords:["thumbsdown","no","dislike","hand"],char:"\u{1f44e}",fitzpatrick_scale:!0,category:"people"},facepunch:{keywords:["angry","violence","fist","hit","attack","hand"],char:"\u{1f44a}",fitzpatrick_scale:!0,category:"people"},fist:{keywords:["fingers","hand","grasp"],char:"\u270a",fitzpatrick_scale:!0,category:"people"},fist_left:{keywords:["hand","fistbump"],char:"\u{1f91b}",fitzpatrick_scale:!0,category:"people"},fist_right:{keywords:["hand","fistbump"],char:"\u{1f91c}",fitzpatrick_scale:!0,category:"people"},v:{keywords:["fingers","ohyeah","hand","peace","victory","two"],char:"\u270c",fitzpatrick_scale:!0,category:"people"},ok_hand:{keywords:["fingers","limbs","perfect","ok","okay"],char:"\u{1f44c}",fitzpatrick_scale:!0,category:"people"},raised_hand:{keywords:["fingers","stop","highfive","palm","ban"],char:"\u270b",fitzpatrick_scale:!0,category:"people"},raised_back_of_hand:{keywords:["fingers","raised","backhand"],char:"\u{1f91a}",fitzpatrick_scale:!0,category:"people"},open_hands:{keywords:["fingers","butterfly","hands","open"],char:"\u{1f450}",fitzpatrick_scale:!0,category:"people"},muscle:{keywords:["arm","flex","hand","summer","strong","biceps"],char:"\u{1f4aa}",fitzpatrick_scale:!0,category:"people"},pray:{keywords:["please","hope","wish","namaste","highfive"],char:"\u{1f64f}",fitzpatrick_scale:!0,category:"people"},foot:{keywords:["kick","stomp"],char:"\u{1f9b6}",fitzpatrick_scale:!0,category:"people"},leg:{keywords:["kick","limb"],char:"\u{1f9b5}",fitzpatrick_scale:!0,category:"people"},handshake:{keywords:["agreement","shake"],char:"\u{1f91d}",fitzpatrick_scale:!1,category:"people"},point_up:{keywords:["hand","fingers","direction","up"],char:"\u261d",fitzpatrick_scale:!0,category:"people"},point_up_2:{keywords:["fingers","hand","direction","up"],char:"\u{1f446}",fitzpatrick_scale:!0,category:"people"},point_down:{keywords:["fingers","hand","direction","down"],char:"\u{1f447}",fitzpatrick_scale:!0,category:"people"},point_left:{keywords:["direction","fingers","hand","left"],char:"\u{1f448}",fitzpatrick_scale:!0,category:"people"},point_right:{keywords:["fingers","hand","direction","right"],char:"\u{1f449}",fitzpatrick_scale:!0,category:"people"},fu:{keywords:["hand","fingers","rude","middle","flipping"],char:"\u{1f595}",fitzpatrick_scale:!0,category:"people"},raised_hand_with_fingers_splayed:{keywords:["hand","fingers","palm"],char:"\u{1f590}",fitzpatrick_scale:!0,category:"people"},love_you:{keywords:["hand","fingers","gesture"],char:"\u{1f91f}",fitzpatrick_scale:!0,category:"people"},metal:{keywords:["hand","fingers","evil_eye","sign_of_horns","rock_on"],char:"\u{1f918}",fitzpatrick_scale:!0,category:"people"},crossed_fingers:{keywords:["good","lucky"],char:"\u{1f91e}",fitzpatrick_scale:!0,category:"people"},vulcan_salute:{keywords:["hand","fingers","spock","star trek"],char:"\u{1f596}",fitzpatrick_scale:!0,category:"people"},writing_hand:{keywords:["lower_left_ballpoint_pen","stationery","write","compose"],char:"\u270d",fitzpatrick_scale:!0,category:"people"},selfie:{keywords:["camera","phone"],char:"\u{1f933}",fitzpatrick_scale:!0,category:"people"},nail_care:{keywords:["beauty","manicure","finger","fashion","nail"],char:"\u{1f485}",fitzpatrick_scale:!0,category:"people"},lips:{keywords:["mouth","kiss"],char:"\u{1f444}",fitzpatrick_scale:!1,category:"people"},tooth:{keywords:["teeth","dentist"],char:"\u{1f9b7}",fitzpatrick_scale:!1,category:"people"},tongue:{keywords:["mouth","playful"],char:"\u{1f445}",fitzpatrick_scale:!1,category:"people"},ear:{keywords:["face","hear","sound","listen"],char:"\u{1f442}",fitzpatrick_scale:!0,category:"people"},nose:{keywords:["smell","sniff"],char:"\u{1f443}",fitzpatrick_scale:!0,category:"people"},eye:{keywords:["face","look","see","watch","stare"],char:"\u{1f441}",fitzpatrick_scale:!1,category:"people"},eyes:{keywords:["look","watch","stalk","peek","see"],char:"\u{1f440}",fitzpatrick_scale:!1,category:"people"},brain:{keywords:["smart","intelligent"],char:"\u{1f9e0}",fitzpatrick_scale:!1,category:"people"},bust_in_silhouette:{keywords:["user","person","human"],char:"\u{1f464}",fitzpatrick_scale:!1,category:"people"},busts_in_silhouette:{keywords:["user","person","human","group","team"],char:"\u{1f465}",fitzpatrick_scale:!1,category:"people"},speaking_head:{keywords:["user","person","human","sing","say","talk"],char:"\u{1f5e3}",fitzpatrick_scale:!1,category:"people"},baby:{keywords:["child","boy","girl","toddler"],char:"\u{1f476}",fitzpatrick_scale:!0,category:"people"},child:{keywords:["gender-neutral","young"],char:"\u{1f9d2}",fitzpatrick_scale:!0,category:"people"},boy:{keywords:["man","male","guy","teenager"],char:"\u{1f466}",fitzpatrick_scale:!0,category:"people"},girl:{keywords:["female","woman","teenager"],char:"\u{1f467}",fitzpatrick_scale:!0,category:"people"},adult:{keywords:["gender-neutral","person"],char:"\u{1f9d1}",fitzpatrick_scale:!0,category:"people"},man:{keywords:["mustache","father","dad","guy","classy","sir","moustache"],char:"\u{1f468}",fitzpatrick_scale:!0,category:"people"},woman:{keywords:["female","girls","lady"],char:"\u{1f469}",fitzpatrick_scale:!0,category:"people"},blonde_woman:{keywords:["woman","female","girl","blonde","person"],char:"\u{1f471}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},blonde_man:{keywords:["man","male","boy","blonde","guy","person"],char:"\u{1f471}",fitzpatrick_scale:!0,category:"people"},bearded_person:{keywords:["person","bewhiskered"],char:"\u{1f9d4}",fitzpatrick_scale:!0,category:"people"},older_adult:{keywords:["human","elder","senior","gender-neutral"],char:"\u{1f9d3}",fitzpatrick_scale:!0,category:"people"},older_man:{keywords:["human","male","men","old","elder","senior"],char:"\u{1f474}",fitzpatrick_scale:!0,category:"people"},older_woman:{keywords:["human","female","women","lady","old","elder","senior"],char:"\u{1f475}",fitzpatrick_scale:!0,category:"people"},man_with_gua_pi_mao:{keywords:["male","boy","chinese"],char:"\u{1f472}",fitzpatrick_scale:!0,category:"people"},woman_with_headscarf:{keywords:["female","hijab","mantilla","tichel"],char:"\u{1f9d5}",fitzpatrick_scale:!0,category:"people"},woman_with_turban:{keywords:["female","indian","hinduism","arabs","woman"],char:"\u{1f473}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},man_with_turban:{keywords:["male","indian","hinduism","arabs"],char:"\u{1f473}",fitzpatrick_scale:!0,category:"people"},policewoman:{keywords:["woman","police","law","legal","enforcement","arrest","911","female"],char:"\u{1f46e}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},policeman:{keywords:["man","police","law","legal","enforcement","arrest","911"],char:"\u{1f46e}",fitzpatrick_scale:!0,category:"people"},construction_worker_woman:{keywords:["female","human","wip","build","construction","worker","labor","woman"],char:"\u{1f477}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},construction_worker_man:{keywords:["male","human","wip","guy","build","construction","worker","labor"],char:"\u{1f477}",fitzpatrick_scale:!0,category:"people"},guardswoman:{keywords:["uk","gb","british","female","royal","woman"],char:"\u{1f482}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},guardsman:{keywords:["uk","gb","british","male","guy","royal"],char:"\u{1f482}",fitzpatrick_scale:!0,category:"people"},female_detective:{keywords:["human","spy","detective","female","woman"],char:"\u{1f575}\ufe0f\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},male_detective:{keywords:["human","spy","detective"],char:"\u{1f575}",fitzpatrick_scale:!0,category:"people"},woman_health_worker:{keywords:["doctor","nurse","therapist","healthcare","woman","human"],char:"\u{1f469}\u200d\u2695\ufe0f",fitzpatrick_scale:!0,category:"people"},man_health_worker:{keywords:["doctor","nurse","therapist","healthcare","man","human"],char:"\u{1f468}\u200d\u2695\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_farmer:{keywords:["rancher","gardener","woman","human"],char:"\u{1f469}\u200d\u{1f33e}",fitzpatrick_scale:!0,category:"people"},man_farmer:{keywords:["rancher","gardener","man","human"],char:"\u{1f468}\u200d\u{1f33e}",fitzpatrick_scale:!0,category:"people"},woman_cook:{keywords:["chef","woman","human"],char:"\u{1f469}\u200d\u{1f373}",fitzpatrick_scale:!0,category:"people"},man_cook:{keywords:["chef","man","human"],char:"\u{1f468}\u200d\u{1f373}",fitzpatrick_scale:!0,category:"people"},woman_student:{keywords:["graduate","woman","human"],char:"\u{1f469}\u200d\u{1f393}",fitzpatrick_scale:!0,category:"people"},man_student:{keywords:["graduate","man","human"],char:"\u{1f468}\u200d\u{1f393}",fitzpatrick_scale:!0,category:"people"},woman_singer:{keywords:["rockstar","entertainer","woman","human"],char:"\u{1f469}\u200d\u{1f3a4}",fitzpatrick_scale:!0,category:"people"},man_singer:{keywords:["rockstar","entertainer","man","human"],char:"\u{1f468}\u200d\u{1f3a4}",fitzpatrick_scale:!0,category:"people"},woman_teacher:{keywords:["instructor","professor","woman","human"],char:"\u{1f469}\u200d\u{1f3eb}",fitzpatrick_scale:!0,category:"people"},man_teacher:{keywords:["instructor","professor","man","human"],char:"\u{1f468}\u200d\u{1f3eb}",fitzpatrick_scale:!0,category:"people"},woman_factory_worker:{keywords:["assembly","industrial","woman","human"],char:"\u{1f469}\u200d\u{1f3ed}",fitzpatrick_scale:!0,category:"people"},man_factory_worker:{keywords:["assembly","industrial","man","human"],char:"\u{1f468}\u200d\u{1f3ed}",fitzpatrick_scale:!0,category:"people"},woman_technologist:{keywords:["coder","developer","engineer","programmer","software","woman","human","laptop","computer"],char:"\u{1f469}\u200d\u{1f4bb}",fitzpatrick_scale:!0,category:"people"},man_technologist:{keywords:["coder","developer","engineer","programmer","software","man","human","laptop","computer"],char:"\u{1f468}\u200d\u{1f4bb}",fitzpatrick_scale:!0,category:"people"},woman_office_worker:{keywords:["business","manager","woman","human"],char:"\u{1f469}\u200d\u{1f4bc}",fitzpatrick_scale:!0,category:"people"},man_office_worker:{keywords:["business","manager","man","human"],char:"\u{1f468}\u200d\u{1f4bc}",fitzpatrick_scale:!0,category:"people"},woman_mechanic:{keywords:["plumber","woman","human","wrench"],char:"\u{1f469}\u200d\u{1f527}",fitzpatrick_scale:!0,category:"people"},man_mechanic:{keywords:["plumber","man","human","wrench"],char:"\u{1f468}\u200d\u{1f527}",fitzpatrick_scale:!0,category:"people"},woman_scientist:{keywords:["biologist","chemist","engineer","physicist","woman","human"],char:"\u{1f469}\u200d\u{1f52c}",fitzpatrick_scale:!0,category:"people"},man_scientist:{keywords:["biologist","chemist","engineer","physicist","man","human"],char:"\u{1f468}\u200d\u{1f52c}",fitzpatrick_scale:!0,category:"people"},woman_artist:{keywords:["painter","woman","human"],char:"\u{1f469}\u200d\u{1f3a8}",fitzpatrick_scale:!0,category:"people"},man_artist:{keywords:["painter","man","human"],char:"\u{1f468}\u200d\u{1f3a8}",fitzpatrick_scale:!0,category:"people"},woman_firefighter:{keywords:["fireman","woman","human"],char:"\u{1f469}\u200d\u{1f692}",fitzpatrick_scale:!0,category:"people"},man_firefighter:{keywords:["fireman","man","human"],char:"\u{1f468}\u200d\u{1f692}",fitzpatrick_scale:!0,category:"people"},woman_pilot:{keywords:["aviator","plane","woman","human"],char:"\u{1f469}\u200d\u2708\ufe0f",fitzpatrick_scale:!0,category:"people"},man_pilot:{keywords:["aviator","plane","man","human"],char:"\u{1f468}\u200d\u2708\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_astronaut:{keywords:["space","rocket","woman","human"],char:"\u{1f469}\u200d\u{1f680}",fitzpatrick_scale:!0,category:"people"},man_astronaut:{keywords:["space","rocket","man","human"],char:"\u{1f468}\u200d\u{1f680}",fitzpatrick_scale:!0,category:"people"},woman_judge:{keywords:["justice","court","woman","human"],char:"\u{1f469}\u200d\u2696\ufe0f",fitzpatrick_scale:!0,category:"people"},man_judge:{keywords:["justice","court","man","human"],char:"\u{1f468}\u200d\u2696\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_superhero:{keywords:["woman","female","good","heroine","superpowers"],char:"\u{1f9b8}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},man_superhero:{keywords:["man","male","good","hero","superpowers"],char:"\u{1f9b8}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_supervillain:{keywords:["woman","female","evil","bad","criminal","heroine","superpowers"],char:"\u{1f9b9}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},man_supervillain:{keywords:["man","male","evil","bad","criminal","hero","superpowers"],char:"\u{1f9b9}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},mrs_claus:{keywords:["woman","female","xmas","mother christmas"],char:"\u{1f936}",fitzpatrick_scale:!0,category:"people"},santa:{keywords:["festival","man","male","xmas","father christmas"],char:"\u{1f385}",fitzpatrick_scale:!0,category:"people"},sorceress:{keywords:["woman","female","mage","witch"],char:"\u{1f9d9}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},wizard:{keywords:["man","male","mage","sorcerer"],char:"\u{1f9d9}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_elf:{keywords:["woman","female"],char:"\u{1f9dd}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},man_elf:{keywords:["man","male"],char:"\u{1f9dd}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_vampire:{keywords:["woman","female"],char:"\u{1f9db}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},man_vampire:{keywords:["man","male","dracula"],char:"\u{1f9db}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_zombie:{keywords:["woman","female","undead","walking dead"],char:"\u{1f9df}\u200d\u2640\ufe0f",fitzpatrick_scale:!1,category:"people"},man_zombie:{keywords:["man","male","dracula","undead","walking dead"],char:"\u{1f9df}\u200d\u2642\ufe0f",fitzpatrick_scale:!1,category:"people"},woman_genie:{keywords:["woman","female"],char:"\u{1f9de}\u200d\u2640\ufe0f",fitzpatrick_scale:!1,category:"people"},man_genie:{keywords:["man","male"],char:"\u{1f9de}\u200d\u2642\ufe0f",fitzpatrick_scale:!1,category:"people"},mermaid:{keywords:["woman","female","merwoman","ariel"],char:"\u{1f9dc}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},merman:{keywords:["man","male","triton"],char:"\u{1f9dc}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_fairy:{keywords:["woman","female"],char:"\u{1f9da}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},man_fairy:{keywords:["man","male"],char:"\u{1f9da}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},angel:{keywords:["heaven","wings","halo"],char:"\u{1f47c}",fitzpatrick_scale:!0,category:"people"},pregnant_woman:{keywords:["baby"],char:"\u{1f930}",fitzpatrick_scale:!0,category:"people"},breastfeeding:{keywords:["nursing","baby"],char:"\u{1f931}",fitzpatrick_scale:!0,category:"people"},princess:{keywords:["girl","woman","female","blond","crown","royal","queen"],char:"\u{1f478}",fitzpatrick_scale:!0,category:"people"},prince:{keywords:["boy","man","male","crown","royal","king"],char:"\u{1f934}",fitzpatrick_scale:!0,category:"people"},bride_with_veil:{keywords:["couple","marriage","wedding","woman","bride"],char:"\u{1f470}",fitzpatrick_scale:!0,category:"people"},man_in_tuxedo:{keywords:["couple","marriage","wedding","groom"],char:"\u{1f935}",fitzpatrick_scale:!0,category:"people"},running_woman:{keywords:["woman","walking","exercise","race","running","female"],char:"\u{1f3c3}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},running_man:{keywords:["man","walking","exercise","race","running"],char:"\u{1f3c3}",fitzpatrick_scale:!0,category:"people"},walking_woman:{keywords:["human","feet","steps","woman","female"],char:"\u{1f6b6}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},walking_man:{keywords:["human","feet","steps"],char:"\u{1f6b6}",fitzpatrick_scale:!0,category:"people"},dancer:{keywords:["female","girl","woman","fun"],char:"\u{1f483}",fitzpatrick_scale:!0,category:"people"},man_dancing:{keywords:["male","boy","fun","dancer"],char:"\u{1f57a}",fitzpatrick_scale:!0,category:"people"},dancing_women:{keywords:["female","bunny","women","girls"],char:"\u{1f46f}",fitzpatrick_scale:!1,category:"people"},dancing_men:{keywords:["male","bunny","men","boys"],char:"\u{1f46f}\u200d\u2642\ufe0f",fitzpatrick_scale:!1,category:"people"},couple:{keywords:["pair","people","human","love","date","dating","like","affection","valentines","marriage"],char:"\u{1f46b}",fitzpatrick_scale:!1,category:"people"},two_men_holding_hands:{keywords:["pair","couple","love","like","bromance","friendship","people","human"],char:"\u{1f46c}",fitzpatrick_scale:!1,category:"people"},two_women_holding_hands:{keywords:["pair","friendship","couple","love","like","female","people","human"],char:"\u{1f46d}",fitzpatrick_scale:!1,category:"people"},bowing_woman:{keywords:["woman","female","girl"],char:"\u{1f647}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},bowing_man:{keywords:["man","male","boy"],char:"\u{1f647}",fitzpatrick_scale:!0,category:"people"},man_facepalming:{keywords:["man","male","boy","disbelief"],char:"\u{1f926}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_facepalming:{keywords:["woman","female","girl","disbelief"],char:"\u{1f926}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_shrugging:{keywords:["woman","female","girl","confused","indifferent","doubt"],char:"\u{1f937}",fitzpatrick_scale:!0,category:"people"},man_shrugging:{keywords:["man","male","boy","confused","indifferent","doubt"],char:"\u{1f937}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},tipping_hand_woman:{keywords:["female","girl","woman","human","information"],char:"\u{1f481}",fitzpatrick_scale:!0,category:"people"},tipping_hand_man:{keywords:["male","boy","man","human","information"],char:"\u{1f481}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},no_good_woman:{keywords:["female","girl","woman","nope"],char:"\u{1f645}",fitzpatrick_scale:!0,category:"people"},no_good_man:{keywords:["male","boy","man","nope"],char:"\u{1f645}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},ok_woman:{keywords:["women","girl","female","pink","human","woman"],char:"\u{1f646}",fitzpatrick_scale:!0,category:"people"},ok_man:{keywords:["men","boy","male","blue","human","man"],char:"\u{1f646}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},raising_hand_woman:{keywords:["female","girl","woman"],char:"\u{1f64b}",fitzpatrick_scale:!0,category:"people"},raising_hand_man:{keywords:["male","boy","man"],char:"\u{1f64b}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},pouting_woman:{keywords:["female","girl","woman"],char:"\u{1f64e}",fitzpatrick_scale:!0,category:"people"},pouting_man:{keywords:["male","boy","man"],char:"\u{1f64e}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},frowning_woman:{keywords:["female","girl","woman","sad","depressed","discouraged","unhappy"],char:"\u{1f64d}",fitzpatrick_scale:!0,category:"people"},frowning_man:{keywords:["male","boy","man","sad","depressed","discouraged","unhappy"],char:"\u{1f64d}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},haircut_woman:{keywords:["female","girl","woman"],char:"\u{1f487}",fitzpatrick_scale:!0,category:"people"},haircut_man:{keywords:["male","boy","man"],char:"\u{1f487}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},massage_woman:{keywords:["female","girl","woman","head"],char:"\u{1f486}",fitzpatrick_scale:!0,category:"people"},massage_man:{keywords:["male","boy","man","head"],char:"\u{1f486}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},woman_in_steamy_room:{keywords:["female","woman","spa","steamroom","sauna"],char:"\u{1f9d6}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"people"},man_in_steamy_room:{keywords:["male","man","spa","steamroom","sauna"],char:"\u{1f9d6}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"people"},couple_with_heart_woman_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"\u{1f491}",fitzpatrick_scale:!1,category:"people"},couple_with_heart_woman_woman:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"\u{1f469}\u200d\u2764\ufe0f\u200d\u{1f469}",fitzpatrick_scale:!1,category:"people"},couple_with_heart_man_man:{keywords:["pair","love","like","affection","human","dating","valentines","marriage"],char:"\u{1f468}\u200d\u2764\ufe0f\u200d\u{1f468}",fitzpatrick_scale:!1,category:"people"},couplekiss_man_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:"\u{1f48f}",fitzpatrick_scale:!1,category:"people"},couplekiss_woman_woman:{keywords:["pair","valentines","love","like","dating","marriage"],char:"\u{1f469}\u200d\u2764\ufe0f\u200d\u{1f48b}\u200d\u{1f469}",fitzpatrick_scale:!1,category:"people"},couplekiss_man_man:{keywords:["pair","valentines","love","like","dating","marriage"],char:"\u{1f468}\u200d\u2764\ufe0f\u200d\u{1f48b}\u200d\u{1f468}",fitzpatrick_scale:!1,category:"people"},family_man_woman_boy:{keywords:["home","parents","child","mom","dad","father","mother","people","human"],char:"\u{1f46a}",fitzpatrick_scale:!1,category:"people"},family_man_woman_girl:{keywords:["home","parents","people","human","child"],char:"\u{1f468}\u200d\u{1f469}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_man_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f469}\u200d\u{1f466}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_woman_woman_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f469}\u200d\u{1f469}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_woman_woman_girl:{keywords:["home","parents","people","human","children"],char:"\u{1f469}\u200d\u{1f469}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_woman_woman_girl_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f469}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_woman_woman_boy_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f469}\u200d\u{1f469}\u200d\u{1f466}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_woman_woman_girl_girl:{keywords:["home","parents","people","human","children"],char:"\u{1f469}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_man_man_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f468}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_man_girl:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f468}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_man_man_girl_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f468}\u200d\u{1f467}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_man_boy_boy:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f468}\u200d\u{1f466}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_man_girl_girl:{keywords:["home","parents","people","human","children"],char:"\u{1f468}\u200d\u{1f468}\u200d\u{1f467}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_woman_boy:{keywords:["home","parent","people","human","child"],char:"\u{1f469}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_woman_girl:{keywords:["home","parent","people","human","child"],char:"\u{1f469}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_woman_girl_boy:{keywords:["home","parent","people","human","children"],char:"\u{1f469}\u200d\u{1f467}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_woman_boy_boy:{keywords:["home","parent","people","human","children"],char:"\u{1f469}\u200d\u{1f466}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_woman_girl_girl:{keywords:["home","parent","people","human","children"],char:"\u{1f469}\u200d\u{1f467}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_man_boy:{keywords:["home","parent","people","human","child"],char:"\u{1f468}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_girl:{keywords:["home","parent","people","human","child"],char:"\u{1f468}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},family_man_girl_boy:{keywords:["home","parent","people","human","children"],char:"\u{1f468}\u200d\u{1f467}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_boy_boy:{keywords:["home","parent","people","human","children"],char:"\u{1f468}\u200d\u{1f466}\u200d\u{1f466}",fitzpatrick_scale:!1,category:"people"},family_man_girl_girl:{keywords:["home","parent","people","human","children"],char:"\u{1f468}\u200d\u{1f467}\u200d\u{1f467}",fitzpatrick_scale:!1,category:"people"},yarn:{keywords:["ball","crochet","knit"],char:"\u{1f9f6}",fitzpatrick_scale:!1,category:"people"},thread:{keywords:["needle","sewing","spool","string"],char:"\u{1f9f5}",fitzpatrick_scale:!1,category:"people"},coat:{keywords:["jacket"],char:"\u{1f9e5}",fitzpatrick_scale:!1,category:"people"},labcoat:{keywords:["doctor","experiment","scientist","chemist"],char:"\u{1f97c}",fitzpatrick_scale:!1,category:"people"},womans_clothes:{keywords:["fashion","shopping_bags","female"],char:"\u{1f45a}",fitzpatrick_scale:!1,category:"people"},tshirt:{keywords:["fashion","cloth","casual","shirt","tee"],char:"\u{1f455}",fitzpatrick_scale:!1,category:"people"},jeans:{keywords:["fashion","shopping"],char:"\u{1f456}",fitzpatrick_scale:!1,category:"people"},necktie:{keywords:["shirt","suitup","formal","fashion","cloth","business"],char:"\u{1f454}",fitzpatrick_scale:!1,category:"people"},dress:{keywords:["clothes","fashion","shopping"],char:"\u{1f457}",fitzpatrick_scale:!1,category:"people"},bikini:{keywords:["swimming","female","woman","girl","fashion","beach","summer"],char:"\u{1f459}",fitzpatrick_scale:!1,category:"people"},kimono:{keywords:["dress","fashion","women","female","japanese"],char:"\u{1f458}",fitzpatrick_scale:!1,category:"people"},lipstick:{keywords:["female","girl","fashion","woman"],char:"\u{1f484}",fitzpatrick_scale:!1,category:"people"},kiss:{keywords:["face","lips","love","like","affection","valentines"],char:"\u{1f48b}",fitzpatrick_scale:!1,category:"people"},footprints:{keywords:["feet","tracking","walking","beach"],char:"\u{1f463}",fitzpatrick_scale:!1,category:"people"},flat_shoe:{keywords:["ballet","slip-on","slipper"],char:"\u{1f97f}",fitzpatrick_scale:!1,category:"people"},high_heel:{keywords:["fashion","shoes","female","pumps","stiletto"],char:"\u{1f460}",fitzpatrick_scale:!1,category:"people"},sandal:{keywords:["shoes","fashion","flip flops"],char:"\u{1f461}",fitzpatrick_scale:!1,category:"people"},boot:{keywords:["shoes","fashion"],char:"\u{1f462}",fitzpatrick_scale:!1,category:"people"},mans_shoe:{keywords:["fashion","male"],char:"\u{1f45e}",fitzpatrick_scale:!1,category:"people"},athletic_shoe:{keywords:["shoes","sports","sneakers"],char:"\u{1f45f}",fitzpatrick_scale:!1,category:"people"},hiking_boot:{keywords:["backpacking","camping","hiking"],char:"\u{1f97e}",fitzpatrick_scale:!1,category:"people"},socks:{keywords:["stockings","clothes"],char:"\u{1f9e6}",fitzpatrick_scale:!1,category:"people"},gloves:{keywords:["hands","winter","clothes"],char:"\u{1f9e4}",fitzpatrick_scale:!1,category:"people"},scarf:{keywords:["neck","winter","clothes"],char:"\u{1f9e3}",fitzpatrick_scale:!1,category:"people"},womans_hat:{keywords:["fashion","accessories","female","lady","spring"],char:"\u{1f452}",fitzpatrick_scale:!1,category:"people"},tophat:{keywords:["magic","gentleman","classy","circus"],char:"\u{1f3a9}",fitzpatrick_scale:!1,category:"people"},billed_hat:{keywords:["cap","baseball"],char:"\u{1f9e2}",fitzpatrick_scale:!1,category:"people"},rescue_worker_helmet:{keywords:["construction","build"],char:"\u26d1",fitzpatrick_scale:!1,category:"people"},mortar_board:{keywords:["school","college","degree","university","graduation","cap","hat","legal","learn","education"],char:"\u{1f393}",fitzpatrick_scale:!1,category:"people"},crown:{keywords:["king","kod","leader","royalty","lord"],char:"\u{1f451}",fitzpatrick_scale:!1,category:"people"},school_satchel:{keywords:["student","education","bag","backpack"],char:"\u{1f392}",fitzpatrick_scale:!1,category:"people"},luggage:{keywords:["packing","travel"],char:"\u{1f9f3}",fitzpatrick_scale:!1,category:"people"},pouch:{keywords:["bag","accessories","shopping"],char:"\u{1f45d}",fitzpatrick_scale:!1,category:"people"},purse:{keywords:["fashion","accessories","money","sales","shopping"],char:"\u{1f45b}",fitzpatrick_scale:!1,category:"people"},handbag:{keywords:["fashion","accessory","accessories","shopping"],char:"\u{1f45c}",fitzpatrick_scale:!1,category:"people"},briefcase:{keywords:["business","documents","work","law","legal","job","career"],char:"\u{1f4bc}",fitzpatrick_scale:!1,category:"people"},eyeglasses:{keywords:["fashion","accessories","eyesight","nerdy","dork","geek"],char:"\u{1f453}",fitzpatrick_scale:!1,category:"people"},dark_sunglasses:{keywords:["face","cool","accessories"],char:"\u{1f576}",fitzpatrick_scale:!1,category:"people"},goggles:{keywords:["eyes","protection","safety"],char:"\u{1f97d}",fitzpatrick_scale:!1,category:"people"},ring:{keywords:["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"],char:"\u{1f48d}",fitzpatrick_scale:!1,category:"people"},closed_umbrella:{keywords:["weather","rain","drizzle"],char:"\u{1f302}",fitzpatrick_scale:!1,category:"people"},dog:{keywords:["animal","friend","nature","woof","puppy","pet","faithful"],char:"\u{1f436}",fitzpatrick_scale:!1,category:"animals_and_nature"},cat:{keywords:["animal","meow","nature","pet","kitten"],char:"\u{1f431}",fitzpatrick_scale:!1,category:"animals_and_nature"},mouse:{keywords:["animal","nature","cheese_wedge","rodent"],char:"\u{1f42d}",fitzpatrick_scale:!1,category:"animals_and_nature"},hamster:{keywords:["animal","nature"],char:"\u{1f439}",fitzpatrick_scale:!1,category:"animals_and_nature"},rabbit:{keywords:["animal","nature","pet","spring","magic","bunny"],char:"\u{1f430}",fitzpatrick_scale:!1,category:"animals_and_nature"},fox_face:{keywords:["animal","nature","face"],char:"\u{1f98a}",fitzpatrick_scale:!1,category:"animals_and_nature"},bear:{keywords:["animal","nature","wild"],char:"\u{1f43b}",fitzpatrick_scale:!1,category:"animals_and_nature"},panda_face:{keywords:["animal","nature","panda"],char:"\u{1f43c}",fitzpatrick_scale:!1,category:"animals_and_nature"},koala:{keywords:["animal","nature"],char:"\u{1f428}",fitzpatrick_scale:!1,category:"animals_and_nature"},tiger:{keywords:["animal","cat","danger","wild","nature","roar"],char:"\u{1f42f}",fitzpatrick_scale:!1,category:"animals_and_nature"},lion:{keywords:["animal","nature"],char:"\u{1f981}",fitzpatrick_scale:!1,category:"animals_and_nature"},cow:{keywords:["beef","ox","animal","nature","moo","milk"],char:"\u{1f42e}",fitzpatrick_scale:!1,category:"animals_and_nature"},pig:{keywords:["animal","oink","nature"],char:"\u{1f437}",fitzpatrick_scale:!1,category:"animals_and_nature"},pig_nose:{keywords:["animal","oink"],char:"\u{1f43d}",fitzpatrick_scale:!1,category:"animals_and_nature"},frog:{keywords:["animal","nature","croak","toad"],char:"\u{1f438}",fitzpatrick_scale:!1,category:"animals_and_nature"},squid:{keywords:["animal","nature","ocean","sea"],char:"\u{1f991}",fitzpatrick_scale:!1,category:"animals_and_nature"},octopus:{keywords:["animal","creature","ocean","sea","nature","beach"],char:"\u{1f419}",fitzpatrick_scale:!1,category:"animals_and_nature"},shrimp:{keywords:["animal","ocean","nature","seafood"],char:"\u{1f990}",fitzpatrick_scale:!1,category:"animals_and_nature"},monkey_face:{keywords:["animal","nature","circus"],char:"\u{1f435}",fitzpatrick_scale:!1,category:"animals_and_nature"},gorilla:{keywords:["animal","nature","circus"],char:"\u{1f98d}",fitzpatrick_scale:!1,category:"animals_and_nature"},see_no_evil:{keywords:["monkey","animal","nature","haha"],char:"\u{1f648}",fitzpatrick_scale:!1,category:"animals_and_nature"},hear_no_evil:{keywords:["animal","monkey","nature"],char:"\u{1f649}",fitzpatrick_scale:!1,category:"animals_and_nature"},speak_no_evil:{keywords:["monkey","animal","nature","omg"],char:"\u{1f64a}",fitzpatrick_scale:!1,category:"animals_and_nature"},monkey:{keywords:["animal","nature","banana","circus"],char:"\u{1f412}",fitzpatrick_scale:!1,category:"animals_and_nature"},chicken:{keywords:["animal","cluck","nature","bird"],char:"\u{1f414}",fitzpatrick_scale:!1,category:"animals_and_nature"},penguin:{keywords:["animal","nature"],char:"\u{1f427}",fitzpatrick_scale:!1,category:"animals_and_nature"},bird:{keywords:["animal","nature","fly","tweet","spring"],char:"\u{1f426}",fitzpatrick_scale:!1,category:"animals_and_nature"},baby_chick:{keywords:["animal","chicken","bird"],char:"\u{1f424}",fitzpatrick_scale:!1,category:"animals_and_nature"},hatching_chick:{keywords:["animal","chicken","egg","born","baby","bird"],char:"\u{1f423}",fitzpatrick_scale:!1,category:"animals_and_nature"},hatched_chick:{keywords:["animal","chicken","baby","bird"],char:"\u{1f425}",fitzpatrick_scale:!1,category:"animals_and_nature"},duck:{keywords:["animal","nature","bird","mallard"],char:"\u{1f986}",fitzpatrick_scale:!1,category:"animals_and_nature"},eagle:{keywords:["animal","nature","bird"],char:"\u{1f985}",fitzpatrick_scale:!1,category:"animals_and_nature"},owl:{keywords:["animal","nature","bird","hoot"],char:"\u{1f989}",fitzpatrick_scale:!1,category:"animals_and_nature"},bat:{keywords:["animal","nature","blind","vampire"],char:"\u{1f987}",fitzpatrick_scale:!1,category:"animals_and_nature"},wolf:{keywords:["animal","nature","wild"],char:"\u{1f43a}",fitzpatrick_scale:!1,category:"animals_and_nature"},boar:{keywords:["animal","nature"],char:"\u{1f417}",fitzpatrick_scale:!1,category:"animals_and_nature"},horse:{keywords:["animal","brown","nature"],char:"\u{1f434}",fitzpatrick_scale:!1,category:"animals_and_nature"},unicorn:{keywords:["animal","nature","mystical"],char:"\u{1f984}",fitzpatrick_scale:!1,category:"animals_and_nature"},honeybee:{keywords:["animal","insect","nature","bug","spring","honey"],char:"\u{1f41d}",fitzpatrick_scale:!1,category:"animals_and_nature"},bug:{keywords:["animal","insect","nature","worm"],char:"\u{1f41b}",fitzpatrick_scale:!1,category:"animals_and_nature"},butterfly:{keywords:["animal","insect","nature","caterpillar"],char:"\u{1f98b}",fitzpatrick_scale:!1,category:"animals_and_nature"},snail:{keywords:["slow","animal","shell"],char:"\u{1f40c}",fitzpatrick_scale:!1,category:"animals_and_nature"},beetle:{keywords:["animal","insect","nature","ladybug"],char:"\u{1f41e}",fitzpatrick_scale:!1,category:"animals_and_nature"},ant:{keywords:["animal","insect","nature","bug"],char:"\u{1f41c}",fitzpatrick_scale:!1,category:"animals_and_nature"},grasshopper:{keywords:["animal","cricket","chirp"],char:"\u{1f997}",fitzpatrick_scale:!1,category:"animals_and_nature"},spider:{keywords:["animal","arachnid"],char:"\u{1f577}",fitzpatrick_scale:!1,category:"animals_and_nature"},scorpion:{keywords:["animal","arachnid"],char:"\u{1f982}",fitzpatrick_scale:!1,category:"animals_and_nature"},crab:{keywords:["animal","crustacean"],char:"\u{1f980}",fitzpatrick_scale:!1,category:"animals_and_nature"},snake:{keywords:["animal","evil","nature","hiss","python"],char:"\u{1f40d}",fitzpatrick_scale:!1,category:"animals_and_nature"},lizard:{keywords:["animal","nature","reptile"],char:"\u{1f98e}",fitzpatrick_scale:!1,category:"animals_and_nature"},"t-rex":{keywords:["animal","nature","dinosaur","tyrannosaurus","extinct"],char:"\u{1f996}",fitzpatrick_scale:!1,category:"animals_and_nature"},sauropod:{keywords:["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"],char:"\u{1f995}",fitzpatrick_scale:!1,category:"animals_and_nature"},turtle:{keywords:["animal","slow","nature","tortoise"],char:"\u{1f422}",fitzpatrick_scale:!1,category:"animals_and_nature"},tropical_fish:{keywords:["animal","swim","ocean","beach","nemo"],char:"\u{1f420}",fitzpatrick_scale:!1,category:"animals_and_nature"},fish:{keywords:["animal","food","nature"],char:"\u{1f41f}",fitzpatrick_scale:!1,category:"animals_and_nature"},blowfish:{keywords:["animal","nature","food","sea","ocean"],char:"\u{1f421}",fitzpatrick_scale:!1,category:"animals_and_nature"},dolphin:{keywords:["animal","nature","fish","sea","ocean","flipper","fins","beach"],char:"\u{1f42c}",fitzpatrick_scale:!1,category:"animals_and_nature"},shark:{keywords:["animal","nature","fish","sea","ocean","jaws","fins","beach"],char:"\u{1f988}",fitzpatrick_scale:!1,category:"animals_and_nature"},whale:{keywords:["animal","nature","sea","ocean"],char:"\u{1f433}",fitzpatrick_scale:!1,category:"animals_and_nature"},whale2:{keywords:["animal","nature","sea","ocean"],char:"\u{1f40b}",fitzpatrick_scale:!1,category:"animals_and_nature"},crocodile:{keywords:["animal","nature","reptile","lizard","alligator"],char:"\u{1f40a}",fitzpatrick_scale:!1,category:"animals_and_nature"},leopard:{keywords:["animal","nature"],char:"\u{1f406}",fitzpatrick_scale:!1,category:"animals_and_nature"},zebra:{keywords:["animal","nature","stripes","safari"],char:"\u{1f993}",fitzpatrick_scale:!1,category:"animals_and_nature"},tiger2:{keywords:["animal","nature","roar"],char:"\u{1f405}",fitzpatrick_scale:!1,category:"animals_and_nature"},water_buffalo:{keywords:["animal","nature","ox","cow"],char:"\u{1f403}",fitzpatrick_scale:!1,category:"animals_and_nature"},ox:{keywords:["animal","cow","beef"],char:"\u{1f402}",fitzpatrick_scale:!1,category:"animals_and_nature"},cow2:{keywords:["beef","ox","animal","nature","moo","milk"],char:"\u{1f404}",fitzpatrick_scale:!1,category:"animals_and_nature"},deer:{keywords:["animal","nature","horns","venison"],char:"\u{1f98c}",fitzpatrick_scale:!1,category:"animals_and_nature"},dromedary_camel:{keywords:["animal","hot","desert","hump"],char:"\u{1f42a}",fitzpatrick_scale:!1,category:"animals_and_nature"},camel:{keywords:["animal","nature","hot","desert","hump"],char:"\u{1f42b}",fitzpatrick_scale:!1,category:"animals_and_nature"},giraffe:{keywords:["animal","nature","spots","safari"],char:"\u{1f992}",fitzpatrick_scale:!1,category:"animals_and_nature"},elephant:{keywords:["animal","nature","nose","th","circus"],char:"\u{1f418}",fitzpatrick_scale:!1,category:"animals_and_nature"},rhinoceros:{keywords:["animal","nature","horn"],char:"\u{1f98f}",fitzpatrick_scale:!1,category:"animals_and_nature"},goat:{keywords:["animal","nature"],char:"\u{1f410}",fitzpatrick_scale:!1,category:"animals_and_nature"},ram:{keywords:["animal","sheep","nature"],char:"\u{1f40f}",fitzpatrick_scale:!1,category:"animals_and_nature"},sheep:{keywords:["animal","nature","wool","shipit"],char:"\u{1f411}",fitzpatrick_scale:!1,category:"animals_and_nature"},racehorse:{keywords:["animal","gamble","luck"],char:"\u{1f40e}",fitzpatrick_scale:!1,category:"animals_and_nature"},pig2:{keywords:["animal","nature"],char:"\u{1f416}",fitzpatrick_scale:!1,category:"animals_and_nature"},rat:{keywords:["animal","mouse","rodent"],char:"\u{1f400}",fitzpatrick_scale:!1,category:"animals_and_nature"},mouse2:{keywords:["animal","nature","rodent"],char:"\u{1f401}",fitzpatrick_scale:!1,category:"animals_and_nature"},rooster:{keywords:["animal","nature","chicken"],char:"\u{1f413}",fitzpatrick_scale:!1,category:"animals_and_nature"},turkey:{keywords:["animal","bird"],char:"\u{1f983}",fitzpatrick_scale:!1,category:"animals_and_nature"},dove:{keywords:["animal","bird"],char:"\u{1f54a}",fitzpatrick_scale:!1,category:"animals_and_nature"},dog2:{keywords:["animal","nature","friend","doge","pet","faithful"],char:"\u{1f415}",fitzpatrick_scale:!1,category:"animals_and_nature"},poodle:{keywords:["dog","animal","101","nature","pet"],char:"\u{1f429}",fitzpatrick_scale:!1,category:"animals_and_nature"},cat2:{keywords:["animal","meow","pet","cats"],char:"\u{1f408}",fitzpatrick_scale:!1,category:"animals_and_nature"},rabbit2:{keywords:["animal","nature","pet","magic","spring"],char:"\u{1f407}",fitzpatrick_scale:!1,category:"animals_and_nature"},chipmunk:{keywords:["animal","nature","rodent","squirrel"],char:"\u{1f43f}",fitzpatrick_scale:!1,category:"animals_and_nature"},hedgehog:{keywords:["animal","nature","spiny"],char:"\u{1f994}",fitzpatrick_scale:!1,category:"animals_and_nature"},raccoon:{keywords:["animal","nature"],char:"\u{1f99d}",fitzpatrick_scale:!1,category:"animals_and_nature"},llama:{keywords:["animal","nature","alpaca"],char:"\u{1f999}",fitzpatrick_scale:!1,category:"animals_and_nature"},hippopotamus:{keywords:["animal","nature"],char:"\u{1f99b}",fitzpatrick_scale:!1,category:"animals_and_nature"},kangaroo:{keywords:["animal","nature","australia","joey","hop","marsupial"],char:"\u{1f998}",fitzpatrick_scale:!1,category:"animals_and_nature"},badger:{keywords:["animal","nature","honey"],char:"\u{1f9a1}",fitzpatrick_scale:!1,category:"animals_and_nature"},swan:{keywords:["animal","nature","bird"],char:"\u{1f9a2}",fitzpatrick_scale:!1,category:"animals_and_nature"},peacock:{keywords:["animal","nature","peahen","bird"],char:"\u{1f99a}",fitzpatrick_scale:!1,category:"animals_and_nature"},parrot:{keywords:["animal","nature","bird","pirate","talk"],char:"\u{1f99c}",fitzpatrick_scale:!1,category:"animals_and_nature"},lobster:{keywords:["animal","nature","bisque","claws","seafood"],char:"\u{1f99e}",fitzpatrick_scale:!1,category:"animals_and_nature"},mosquito:{keywords:["animal","nature","insect","malaria"],char:"\u{1f99f}",fitzpatrick_scale:!1,category:"animals_and_nature"},paw_prints:{keywords:["animal","tracking","footprints","dog","cat","pet","feet"],char:"\u{1f43e}",fitzpatrick_scale:!1,category:"animals_and_nature"},dragon:{keywords:["animal","myth","nature","chinese","green"],char:"\u{1f409}",fitzpatrick_scale:!1,category:"animals_and_nature"},dragon_face:{keywords:["animal","myth","nature","chinese","green"],char:"\u{1f432}",fitzpatrick_scale:!1,category:"animals_and_nature"},cactus:{keywords:["vegetable","plant","nature"],char:"\u{1f335}",fitzpatrick_scale:!1,category:"animals_and_nature"},christmas_tree:{keywords:["festival","vacation","december","xmas","celebration"],char:"\u{1f384}",fitzpatrick_scale:!1,category:"animals_and_nature"},evergreen_tree:{keywords:["plant","nature"],char:"\u{1f332}",fitzpatrick_scale:!1,category:"animals_and_nature"},deciduous_tree:{keywords:["plant","nature"],char:"\u{1f333}",fitzpatrick_scale:!1,category:"animals_and_nature"},palm_tree:{keywords:["plant","vegetable","nature","summer","beach","mojito","tropical"],char:"\u{1f334}",fitzpatrick_scale:!1,category:"animals_and_nature"},seedling:{keywords:["plant","nature","grass","lawn","spring"],char:"\u{1f331}",fitzpatrick_scale:!1,category:"animals_and_nature"},herb:{keywords:["vegetable","plant","medicine","weed","grass","lawn"],char:"\u{1f33f}",fitzpatrick_scale:!1,category:"animals_and_nature"},shamrock:{keywords:["vegetable","plant","nature","irish","clover"],char:"\u2618",fitzpatrick_scale:!1,category:"animals_and_nature"},four_leaf_clover:{keywords:["vegetable","plant","nature","lucky","irish"],char:"\u{1f340}",fitzpatrick_scale:!1,category:"animals_and_nature"},bamboo:{keywords:["plant","nature","vegetable","panda","pine_decoration"],char:"\u{1f38d}",fitzpatrick_scale:!1,category:"animals_and_nature"},tanabata_tree:{keywords:["plant","nature","branch","summer"],char:"\u{1f38b}",fitzpatrick_scale:!1,category:"animals_and_nature"},leaves:{keywords:["nature","plant","tree","vegetable","grass","lawn","spring"],char:"\u{1f343}",fitzpatrick_scale:!1,category:"animals_and_nature"},fallen_leaf:{keywords:["nature","plant","vegetable","leaves"],char:"\u{1f342}",fitzpatrick_scale:!1,category:"animals_and_nature"},maple_leaf:{keywords:["nature","plant","vegetable","ca","fall"],char:"\u{1f341}",fitzpatrick_scale:!1,category:"animals_and_nature"},ear_of_rice:{keywords:["nature","plant"],char:"\u{1f33e}",fitzpatrick_scale:!1,category:"animals_and_nature"},hibiscus:{keywords:["plant","vegetable","flowers","beach"],char:"\u{1f33a}",fitzpatrick_scale:!1,category:"animals_and_nature"},sunflower:{keywords:["nature","plant","fall"],char:"\u{1f33b}",fitzpatrick_scale:!1,category:"animals_and_nature"},rose:{keywords:["flowers","valentines","love","spring"],char:"\u{1f339}",fitzpatrick_scale:!1,category:"animals_and_nature"},wilted_flower:{keywords:["plant","nature","flower"],char:"\u{1f940}",fitzpatrick_scale:!1,category:"animals_and_nature"},tulip:{keywords:["flowers","plant","nature","summer","spring"],char:"\u{1f337}",fitzpatrick_scale:!1,category:"animals_and_nature"},blossom:{keywords:["nature","flowers","yellow"],char:"\u{1f33c}",fitzpatrick_scale:!1,category:"animals_and_nature"},cherry_blossom:{keywords:["nature","plant","spring","flower"],char:"\u{1f338}",fitzpatrick_scale:!1,category:"animals_and_nature"},bouquet:{keywords:["flowers","nature","spring"],char:"\u{1f490}",fitzpatrick_scale:!1,category:"animals_and_nature"},mushroom:{keywords:["plant","vegetable"],char:"\u{1f344}",fitzpatrick_scale:!1,category:"animals_and_nature"},chestnut:{keywords:["food","squirrel"],char:"\u{1f330}",fitzpatrick_scale:!1,category:"animals_and_nature"},jack_o_lantern:{keywords:["halloween","light","pumpkin","creepy","fall"],char:"\u{1f383}",fitzpatrick_scale:!1,category:"animals_and_nature"},shell:{keywords:["nature","sea","beach"],char:"\u{1f41a}",fitzpatrick_scale:!1,category:"animals_and_nature"},spider_web:{keywords:["animal","insect","arachnid","silk"],char:"\u{1f578}",fitzpatrick_scale:!1,category:"animals_and_nature"},earth_americas:{keywords:["globe","world","USA","international"],char:"\u{1f30e}",fitzpatrick_scale:!1,category:"animals_and_nature"},earth_africa:{keywords:["globe","world","international"],char:"\u{1f30d}",fitzpatrick_scale:!1,category:"animals_and_nature"},earth_asia:{keywords:["globe","world","east","international"],char:"\u{1f30f}",fitzpatrick_scale:!1,category:"animals_and_nature"},full_moon:{keywords:["nature","yellow","twilight","planet","space","night","evening","sleep"],char:"\u{1f315}",fitzpatrick_scale:!1,category:"animals_and_nature"},waning_gibbous_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"],char:"\u{1f316}",fitzpatrick_scale:!1,category:"animals_and_nature"},last_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f317}",fitzpatrick_scale:!1,category:"animals_and_nature"},waning_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f318}",fitzpatrick_scale:!1,category:"animals_and_nature"},new_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f311}",fitzpatrick_scale:!1,category:"animals_and_nature"},waxing_crescent_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f312}",fitzpatrick_scale:!1,category:"animals_and_nature"},first_quarter_moon:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f313}",fitzpatrick_scale:!1,category:"animals_and_nature"},waxing_gibbous_moon:{keywords:["nature","night","sky","gray","twilight","planet","space","evening","sleep"],char:"\u{1f314}",fitzpatrick_scale:!1,category:"animals_and_nature"},new_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f31a}",fitzpatrick_scale:!1,category:"animals_and_nature"},full_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f31d}",fitzpatrick_scale:!1,category:"animals_and_nature"},first_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f31b}",fitzpatrick_scale:!1,category:"animals_and_nature"},last_quarter_moon_with_face:{keywords:["nature","twilight","planet","space","night","evening","sleep"],char:"\u{1f31c}",fitzpatrick_scale:!1,category:"animals_and_nature"},sun_with_face:{keywords:["nature","morning","sky"],char:"\u{1f31e}",fitzpatrick_scale:!1,category:"animals_and_nature"},crescent_moon:{keywords:["night","sleep","sky","evening","magic"],char:"\u{1f319}",fitzpatrick_scale:!1,category:"animals_and_nature"},star:{keywords:["night","yellow"],char:"\u2b50",fitzpatrick_scale:!1,category:"animals_and_nature"},star2:{keywords:["night","sparkle","awesome","good","magic"],char:"\u{1f31f}",fitzpatrick_scale:!1,category:"animals_and_nature"},dizzy:{keywords:["star","sparkle","shoot","magic"],char:"\u{1f4ab}",fitzpatrick_scale:!1,category:"animals_and_nature"},sparkles:{keywords:["stars","shine","shiny","cool","awesome","good","magic"],char:"\u2728",fitzpatrick_scale:!1,category:"animals_and_nature"},comet:{keywords:["space"],char:"\u2604",fitzpatrick_scale:!1,category:"animals_and_nature"},sunny:{keywords:["weather","nature","brightness","summer","beach","spring"],char:"\u2600\ufe0f",fitzpatrick_scale:!1,category:"animals_and_nature"},sun_behind_small_cloud:{keywords:["weather"],char:"\u{1f324}",fitzpatrick_scale:!1,category:"animals_and_nature"},partly_sunny:{keywords:["weather","nature","cloudy","morning","fall","spring"],char:"\u26c5",fitzpatrick_scale:!1,category:"animals_and_nature"},sun_behind_large_cloud:{keywords:["weather"],char:"\u{1f325}",fitzpatrick_scale:!1,category:"animals_and_nature"},sun_behind_rain_cloud:{keywords:["weather"],char:"\u{1f326}",fitzpatrick_scale:!1,category:"animals_and_nature"},cloud:{keywords:["weather","sky"],char:"\u2601\ufe0f",fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_rain:{keywords:["weather"],char:"\u{1f327}",fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_lightning_and_rain:{keywords:["weather","lightning"],char:"\u26c8",fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_lightning:{keywords:["weather","thunder"],char:"\u{1f329}",fitzpatrick_scale:!1,category:"animals_and_nature"},zap:{keywords:["thunder","weather","lightning bolt","fast"],char:"\u26a1",fitzpatrick_scale:!1,category:"animals_and_nature"},fire:{keywords:["hot","cook","flame"],char:"\u{1f525}",fitzpatrick_scale:!1,category:"animals_and_nature"},boom:{keywords:["bomb","explode","explosion","collision","blown"],char:"\u{1f4a5}",fitzpatrick_scale:!1,category:"animals_and_nature"},snowflake:{keywords:["winter","season","cold","weather","christmas","xmas"],char:"\u2744\ufe0f",fitzpatrick_scale:!1,category:"animals_and_nature"},cloud_with_snow:{keywords:["weather"],char:"\u{1f328}",fitzpatrick_scale:!1,category:"animals_and_nature"},snowman:{keywords:["winter","season","cold","weather","christmas","xmas","frozen","without_snow"],char:"\u26c4",fitzpatrick_scale:!1,category:"animals_and_nature"},snowman_with_snow:{keywords:["winter","season","cold","weather","christmas","xmas","frozen"],char:"\u2603",fitzpatrick_scale:!1,category:"animals_and_nature"},wind_face:{keywords:["gust","air"],char:"\u{1f32c}",fitzpatrick_scale:!1,category:"animals_and_nature"},dash:{keywords:["wind","air","fast","shoo","fart","smoke","puff"],char:"\u{1f4a8}",fitzpatrick_scale:!1,category:"animals_and_nature"},tornado:{keywords:["weather","cyclone","twister"],char:"\u{1f32a}",fitzpatrick_scale:!1,category:"animals_and_nature"},fog:{keywords:["weather"],char:"\u{1f32b}",fitzpatrick_scale:!1,category:"animals_and_nature"},open_umbrella:{keywords:["weather","spring"],char:"\u2602",fitzpatrick_scale:!1,category:"animals_and_nature"},umbrella:{keywords:["rainy","weather","spring"],char:"\u2614",fitzpatrick_scale:!1,category:"animals_and_nature"},droplet:{keywords:["water","drip","faucet","spring"],char:"\u{1f4a7}",fitzpatrick_scale:!1,category:"animals_and_nature"},sweat_drops:{keywords:["water","drip","oops"],char:"\u{1f4a6}",fitzpatrick_scale:!1,category:"animals_and_nature"},ocean:{keywords:["sea","water","wave","nature","tsunami","disaster"],char:"\u{1f30a}",fitzpatrick_scale:!1,category:"animals_and_nature"},green_apple:{keywords:["fruit","nature"],char:"\u{1f34f}",fitzpatrick_scale:!1,category:"food_and_drink"},apple:{keywords:["fruit","mac","school"],char:"\u{1f34e}",fitzpatrick_scale:!1,category:"food_and_drink"},pear:{keywords:["fruit","nature","food"],char:"\u{1f350}",fitzpatrick_scale:!1,category:"food_and_drink"},tangerine:{keywords:["food","fruit","nature","orange"],char:"\u{1f34a}",fitzpatrick_scale:!1,category:"food_and_drink"},lemon:{keywords:["fruit","nature"],char:"\u{1f34b}",fitzpatrick_scale:!1,category:"food_and_drink"},banana:{keywords:["fruit","food","monkey"],char:"\u{1f34c}",fitzpatrick_scale:!1,category:"food_and_drink"},watermelon:{keywords:["fruit","food","picnic","summer"],char:"\u{1f349}",fitzpatrick_scale:!1,category:"food_and_drink"},grapes:{keywords:["fruit","food","wine"],char:"\u{1f347}",fitzpatrick_scale:!1,category:"food_and_drink"},strawberry:{keywords:["fruit","food","nature"],char:"\u{1f353}",fitzpatrick_scale:!1,category:"food_and_drink"},melon:{keywords:["fruit","nature","food"],char:"\u{1f348}",fitzpatrick_scale:!1,category:"food_and_drink"},cherries:{keywords:["food","fruit"],char:"\u{1f352}",fitzpatrick_scale:!1,category:"food_and_drink"},peach:{keywords:["fruit","nature","food"],char:"\u{1f351}",fitzpatrick_scale:!1,category:"food_and_drink"},pineapple:{keywords:["fruit","nature","food"],char:"\u{1f34d}",fitzpatrick_scale:!1,category:"food_and_drink"},coconut:{keywords:["fruit","nature","food","palm"],char:"\u{1f965}",fitzpatrick_scale:!1,category:"food_and_drink"},kiwi_fruit:{keywords:["fruit","food"],char:"\u{1f95d}",fitzpatrick_scale:!1,category:"food_and_drink"},mango:{keywords:["fruit","food","tropical"],char:"\u{1f96d}",fitzpatrick_scale:!1,category:"food_and_drink"},avocado:{keywords:["fruit","food"],char:"\u{1f951}",fitzpatrick_scale:!1,category:"food_and_drink"},broccoli:{keywords:["fruit","food","vegetable"],char:"\u{1f966}",fitzpatrick_scale:!1,category:"food_and_drink"},tomato:{keywords:["fruit","vegetable","nature","food"],char:"\u{1f345}",fitzpatrick_scale:!1,category:"food_and_drink"},eggplant:{keywords:["vegetable","nature","food","aubergine"],char:"\u{1f346}",fitzpatrick_scale:!1,category:"food_and_drink"},cucumber:{keywords:["fruit","food","pickle"],char:"\u{1f952}",fitzpatrick_scale:!1,category:"food_and_drink"},carrot:{keywords:["vegetable","food","orange"],char:"\u{1f955}",fitzpatrick_scale:!1,category:"food_and_drink"},hot_pepper:{keywords:["food","spicy","chilli","chili"],char:"\u{1f336}",fitzpatrick_scale:!1,category:"food_and_drink"},potato:{keywords:["food","tuber","vegatable","starch"],char:"\u{1f954}",fitzpatrick_scale:!1,category:"food_and_drink"},corn:{keywords:["food","vegetable","plant"],char:"\u{1f33d}",fitzpatrick_scale:!1,category:"food_and_drink"},leafy_greens:{keywords:["food","vegetable","plant","bok choy","cabbage","kale","lettuce"],char:"\u{1f96c}",fitzpatrick_scale:!1,category:"food_and_drink"},sweet_potato:{keywords:["food","nature"],char:"\u{1f360}",fitzpatrick_scale:!1,category:"food_and_drink"},peanuts:{keywords:["food","nut"],char:"\u{1f95c}",fitzpatrick_scale:!1,category:"food_and_drink"},honey_pot:{keywords:["bees","sweet","kitchen"],char:"\u{1f36f}",fitzpatrick_scale:!1,category:"food_and_drink"},croissant:{keywords:["food","bread","french"],char:"\u{1f950}",fitzpatrick_scale:!1,category:"food_and_drink"},bread:{keywords:["food","wheat","breakfast","toast"],char:"\u{1f35e}",fitzpatrick_scale:!1,category:"food_and_drink"},baguette_bread:{keywords:["food","bread","french"],char:"\u{1f956}",fitzpatrick_scale:!1,category:"food_and_drink"},bagel:{keywords:["food","bread","bakery","schmear"],char:"\u{1f96f}",fitzpatrick_scale:!1,category:"food_and_drink"},pretzel:{keywords:["food","bread","twisted"],char:"\u{1f968}",fitzpatrick_scale:!1,category:"food_and_drink"},cheese:{keywords:["food","chadder"],char:"\u{1f9c0}",fitzpatrick_scale:!1,category:"food_and_drink"},egg:{keywords:["food","chicken","breakfast"],char:"\u{1f95a}",fitzpatrick_scale:!1,category:"food_and_drink"},bacon:{keywords:["food","breakfast","pork","pig","meat"],char:"\u{1f953}",fitzpatrick_scale:!1,category:"food_and_drink"},steak:{keywords:["food","cow","meat","cut","chop","lambchop","porkchop"],char:"\u{1f969}",fitzpatrick_scale:!1,category:"food_and_drink"},pancakes:{keywords:["food","breakfast","flapjacks","hotcakes"],char:"\u{1f95e}",fitzpatrick_scale:!1,category:"food_and_drink"},poultry_leg:{keywords:["food","meat","drumstick","bird","chicken","turkey"],char:"\u{1f357}",fitzpatrick_scale:!1,category:"food_and_drink"},meat_on_bone:{keywords:["good","food","drumstick"],char:"\u{1f356}",fitzpatrick_scale:!1,category:"food_and_drink"},bone:{keywords:["skeleton"],char:"\u{1f9b4}",fitzpatrick_scale:!1,category:"food_and_drink"},fried_shrimp:{keywords:["food","animal","appetizer","summer"],char:"\u{1f364}",fitzpatrick_scale:!1,category:"food_and_drink"},fried_egg:{keywords:["food","breakfast","kitchen","egg"],char:"\u{1f373}",fitzpatrick_scale:!1,category:"food_and_drink"},hamburger:{keywords:["meat","fast food","beef","cheeseburger","mcdonalds","burger king"],char:"\u{1f354}",fitzpatrick_scale:!1,category:"food_and_drink"},fries:{keywords:["chips","snack","fast food"],char:"\u{1f35f}",fitzpatrick_scale:!1,category:"food_and_drink"},stuffed_flatbread:{keywords:["food","flatbread","stuffed","gyro"],char:"\u{1f959}",fitzpatrick_scale:!1,category:"food_and_drink"},hotdog:{keywords:["food","frankfurter"],char:"\u{1f32d}",fitzpatrick_scale:!1,category:"food_and_drink"},pizza:{keywords:["food","party"],char:"\u{1f355}",fitzpatrick_scale:!1,category:"food_and_drink"},sandwich:{keywords:["food","lunch","bread"],char:"\u{1f96a}",fitzpatrick_scale:!1,category:"food_and_drink"},canned_food:{keywords:["food","soup"],char:"\u{1f96b}",fitzpatrick_scale:!1,category:"food_and_drink"},spaghetti:{keywords:["food","italian","noodle"],char:"\u{1f35d}",fitzpatrick_scale:!1,category:"food_and_drink"},taco:{keywords:["food","mexican"],char:"\u{1f32e}",fitzpatrick_scale:!1,category:"food_and_drink"},burrito:{keywords:["food","mexican"],char:"\u{1f32f}",fitzpatrick_scale:!1,category:"food_and_drink"},green_salad:{keywords:["food","healthy","lettuce"],char:"\u{1f957}",fitzpatrick_scale:!1,category:"food_and_drink"},shallow_pan_of_food:{keywords:["food","cooking","casserole","paella"],char:"\u{1f958}",fitzpatrick_scale:!1,category:"food_and_drink"},ramen:{keywords:["food","japanese","noodle","chopsticks"],char:"\u{1f35c}",fitzpatrick_scale:!1,category:"food_and_drink"},stew:{keywords:["food","meat","soup"],char:"\u{1f372}",fitzpatrick_scale:!1,category:"food_and_drink"},fish_cake:{keywords:["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"],char:"\u{1f365}",fitzpatrick_scale:!1,category:"food_and_drink"},fortune_cookie:{keywords:["food","prophecy"],char:"\u{1f960}",fitzpatrick_scale:!1,category:"food_and_drink"},sushi:{keywords:["food","fish","japanese","rice"],char:"\u{1f363}",fitzpatrick_scale:!1,category:"food_and_drink"},bento:{keywords:["food","japanese","box"],char:"\u{1f371}",fitzpatrick_scale:!1,category:"food_and_drink"},curry:{keywords:["food","spicy","hot","indian"],char:"\u{1f35b}",fitzpatrick_scale:!1,category:"food_and_drink"},rice_ball:{keywords:["food","japanese"],char:"\u{1f359}",fitzpatrick_scale:!1,category:"food_and_drink"},rice:{keywords:["food","china","asian"],char:"\u{1f35a}",fitzpatrick_scale:!1,category:"food_and_drink"},rice_cracker:{keywords:["food","japanese"],char:"\u{1f358}",fitzpatrick_scale:!1,category:"food_and_drink"},oden:{keywords:["food","japanese"],char:"\u{1f362}",fitzpatrick_scale:!1,category:"food_and_drink"},dango:{keywords:["food","dessert","sweet","japanese","barbecue","meat"],char:"\u{1f361}",fitzpatrick_scale:!1,category:"food_and_drink"},shaved_ice:{keywords:["hot","dessert","summer"],char:"\u{1f367}",fitzpatrick_scale:!1,category:"food_and_drink"},ice_cream:{keywords:["food","hot","dessert"],char:"\u{1f368}",fitzpatrick_scale:!1,category:"food_and_drink"},icecream:{keywords:["food","hot","dessert","summer"],char:"\u{1f366}",fitzpatrick_scale:!1,category:"food_and_drink"},pie:{keywords:["food","dessert","pastry"],char:"\u{1f967}",fitzpatrick_scale:!1,category:"food_and_drink"},cake:{keywords:["food","dessert"],char:"\u{1f370}",fitzpatrick_scale:!1,category:"food_and_drink"},cupcake:{keywords:["food","dessert","bakery","sweet"],char:"\u{1f9c1}",fitzpatrick_scale:!1,category:"food_and_drink"},moon_cake:{keywords:["food","autumn"],char:"\u{1f96e}",fitzpatrick_scale:!1,category:"food_and_drink"},birthday:{keywords:["food","dessert","cake"],char:"\u{1f382}",fitzpatrick_scale:!1,category:"food_and_drink"},custard:{keywords:["dessert","food"],char:"\u{1f36e}",fitzpatrick_scale:!1,category:"food_and_drink"},candy:{keywords:["snack","dessert","sweet","lolly"],char:"\u{1f36c}",fitzpatrick_scale:!1,category:"food_and_drink"},lollipop:{keywords:["food","snack","candy","sweet"],char:"\u{1f36d}",fitzpatrick_scale:!1,category:"food_and_drink"},chocolate_bar:{keywords:["food","snack","dessert","sweet"],char:"\u{1f36b}",fitzpatrick_scale:!1,category:"food_and_drink"},popcorn:{keywords:["food","movie theater","films","snack"],char:"\u{1f37f}",fitzpatrick_scale:!1,category:"food_and_drink"},dumpling:{keywords:["food","empanada","pierogi","potsticker"],char:"\u{1f95f}",fitzpatrick_scale:!1,category:"food_and_drink"},doughnut:{keywords:["food","dessert","snack","sweet","donut"],char:"\u{1f369}",fitzpatrick_scale:!1,category:"food_and_drink"},cookie:{keywords:["food","snack","oreo","chocolate","sweet","dessert"],char:"\u{1f36a}",fitzpatrick_scale:!1,category:"food_and_drink"},milk_glass:{keywords:["beverage","drink","cow"],char:"\u{1f95b}",fitzpatrick_scale:!1,category:"food_and_drink"},beer:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:"\u{1f37a}",fitzpatrick_scale:!1,category:"food_and_drink"},beers:{keywords:["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],char:"\u{1f37b}",fitzpatrick_scale:!1,category:"food_and_drink"},clinking_glasses:{keywords:["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"],char:"\u{1f942}",fitzpatrick_scale:!1,category:"food_and_drink"},wine_glass:{keywords:["drink","beverage","drunk","alcohol","booze"],char:"\u{1f377}",fitzpatrick_scale:!1,category:"food_and_drink"},tumbler_glass:{keywords:["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"],char:"\u{1f943}",fitzpatrick_scale:!1,category:"food_and_drink"},cocktail:{keywords:["drink","drunk","alcohol","beverage","booze","mojito"],char:"\u{1f378}",fitzpatrick_scale:!1,category:"food_and_drink"},tropical_drink:{keywords:["beverage","cocktail","summer","beach","alcohol","booze","mojito"],char:"\u{1f379}",fitzpatrick_scale:!1,category:"food_and_drink"},champagne:{keywords:["drink","wine","bottle","celebration"],char:"\u{1f37e}",fitzpatrick_scale:!1,category:"food_and_drink"},sake:{keywords:["wine","drink","drunk","beverage","japanese","alcohol","booze"],char:"\u{1f376}",fitzpatrick_scale:!1,category:"food_and_drink"},tea:{keywords:["drink","bowl","breakfast","green","british"],char:"\u{1f375}",fitzpatrick_scale:!1,category:"food_and_drink"},cup_with_straw:{keywords:["drink","soda"],char:"\u{1f964}",fitzpatrick_scale:!1,category:"food_and_drink"},coffee:{keywords:["beverage","caffeine","latte","espresso"],char:"\u2615",fitzpatrick_scale:!1,category:"food_and_drink"},baby_bottle:{keywords:["food","container","milk"],char:"\u{1f37c}",fitzpatrick_scale:!1,category:"food_and_drink"},salt:{keywords:["condiment","shaker"],char:"\u{1f9c2}",fitzpatrick_scale:!1,category:"food_and_drink"},spoon:{keywords:["cutlery","kitchen","tableware"],char:"\u{1f944}",fitzpatrick_scale:!1,category:"food_and_drink"},fork_and_knife:{keywords:["cutlery","kitchen"],char:"\u{1f374}",fitzpatrick_scale:!1,category:"food_and_drink"},plate_with_cutlery:{keywords:["food","eat","meal","lunch","dinner","restaurant"],char:"\u{1f37d}",fitzpatrick_scale:!1,category:"food_and_drink"},bowl_with_spoon:{keywords:["food","breakfast","cereal","oatmeal","porridge"],char:"\u{1f963}",fitzpatrick_scale:!1,category:"food_and_drink"},takeout_box:{keywords:["food","leftovers"],char:"\u{1f961}",fitzpatrick_scale:!1,category:"food_and_drink"},chopsticks:{keywords:["food"],char:"\u{1f962}",fitzpatrick_scale:!1,category:"food_and_drink"},soccer:{keywords:["sports","football"],char:"\u26bd",fitzpatrick_scale:!1,category:"activity"},basketball:{keywords:["sports","balls","NBA"],char:"\u{1f3c0}",fitzpatrick_scale:!1,category:"activity"},football:{keywords:["sports","balls","NFL"],char:"\u{1f3c8}",fitzpatrick_scale:!1,category:"activity"},baseball:{keywords:["sports","balls"],char:"\u26be",fitzpatrick_scale:!1,category:"activity"},softball:{keywords:["sports","balls"],char:"\u{1f94e}",fitzpatrick_scale:!1,category:"activity"},tennis:{keywords:["sports","balls","green"],char:"\u{1f3be}",fitzpatrick_scale:!1,category:"activity"},volleyball:{keywords:["sports","balls"],char:"\u{1f3d0}",fitzpatrick_scale:!1,category:"activity"},rugby_football:{keywords:["sports","team"],char:"\u{1f3c9}",fitzpatrick_scale:!1,category:"activity"},flying_disc:{keywords:["sports","frisbee","ultimate"],char:"\u{1f94f}",fitzpatrick_scale:!1,category:"activity"},"8ball":{keywords:["pool","hobby","game","luck","magic"],char:"\u{1f3b1}",fitzpatrick_scale:!1,category:"activity"},golf:{keywords:["sports","business","flag","hole","summer"],char:"\u26f3",fitzpatrick_scale:!1,category:"activity"},golfing_woman:{keywords:["sports","business","woman","female"],char:"\u{1f3cc}\ufe0f\u200d\u2640\ufe0f",fitzpatrick_scale:!1,category:"activity"},golfing_man:{keywords:["sports","business"],char:"\u{1f3cc}",fitzpatrick_scale:!0,category:"activity"},ping_pong:{keywords:["sports","pingpong"],char:"\u{1f3d3}",fitzpatrick_scale:!1,category:"activity"},badminton:{keywords:["sports"],char:"\u{1f3f8}",fitzpatrick_scale:!1,category:"activity"},goal_net:{keywords:["sports"],char:"\u{1f945}",fitzpatrick_scale:!1,category:"activity"},ice_hockey:{keywords:["sports"],char:"\u{1f3d2}",fitzpatrick_scale:!1,category:"activity"},field_hockey:{keywords:["sports"],char:"\u{1f3d1}",fitzpatrick_scale:!1,category:"activity"},lacrosse:{keywords:["sports","ball","stick"],char:"\u{1f94d}",fitzpatrick_scale:!1,category:"activity"},cricket:{keywords:["sports"],char:"\u{1f3cf}",fitzpatrick_scale:!1,category:"activity"},ski:{keywords:["sports","winter","cold","snow"],char:"\u{1f3bf}",fitzpatrick_scale:!1,category:"activity"},skier:{keywords:["sports","winter","snow"],char:"\u26f7",fitzpatrick_scale:!1,category:"activity"},snowboarder:{keywords:["sports","winter"],char:"\u{1f3c2}",fitzpatrick_scale:!0,category:"activity"},person_fencing:{keywords:["sports","fencing","sword"],char:"\u{1f93a}",fitzpatrick_scale:!1,category:"activity"},women_wrestling:{keywords:["sports","wrestlers"],char:"\u{1f93c}\u200d\u2640\ufe0f",fitzpatrick_scale:!1,category:"activity"},men_wrestling:{keywords:["sports","wrestlers"],char:"\u{1f93c}\u200d\u2642\ufe0f",fitzpatrick_scale:!1,category:"activity"},woman_cartwheeling:{keywords:["gymnastics"],char:"\u{1f938}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},man_cartwheeling:{keywords:["gymnastics"],char:"\u{1f938}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"activity"},woman_playing_handball:{keywords:["sports"],char:"\u{1f93e}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},man_playing_handball:{keywords:["sports"],char:"\u{1f93e}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"activity"},ice_skate:{keywords:["sports"],char:"\u26f8",fitzpatrick_scale:!1,category:"activity"},curling_stone:{keywords:["sports"],char:"\u{1f94c}",fitzpatrick_scale:!1,category:"activity"},skateboard:{keywords:["board"],char:"\u{1f6f9}",fitzpatrick_scale:!1,category:"activity"},sled:{keywords:["sleigh","luge","toboggan"],char:"\u{1f6f7}",fitzpatrick_scale:!1,category:"activity"},bow_and_arrow:{keywords:["sports"],char:"\u{1f3f9}",fitzpatrick_scale:!1,category:"activity"},fishing_pole_and_fish:{keywords:["food","hobby","summer"],char:"\u{1f3a3}",fitzpatrick_scale:!1,category:"activity"},boxing_glove:{keywords:["sports","fighting"],char:"\u{1f94a}",fitzpatrick_scale:!1,category:"activity"},martial_arts_uniform:{keywords:["judo","karate","taekwondo"],char:"\u{1f94b}",fitzpatrick_scale:!1,category:"activity"},rowing_woman:{keywords:["sports","hobby","water","ship","woman","female"],char:"\u{1f6a3}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},rowing_man:{keywords:["sports","hobby","water","ship"],char:"\u{1f6a3}",fitzpatrick_scale:!0,category:"activity"},climbing_woman:{keywords:["sports","hobby","woman","female","rock"],char:"\u{1f9d7}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},climbing_man:{keywords:["sports","hobby","man","male","rock"],char:"\u{1f9d7}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"activity"},swimming_woman:{keywords:["sports","exercise","human","athlete","water","summer","woman","female"],char:"\u{1f3ca}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},swimming_man:{keywords:["sports","exercise","human","athlete","water","summer"],char:"\u{1f3ca}",fitzpatrick_scale:!0,category:"activity"},woman_playing_water_polo:{keywords:["sports","pool"],char:"\u{1f93d}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},man_playing_water_polo:{keywords:["sports","pool"],char:"\u{1f93d}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"activity"},woman_in_lotus_position:{keywords:["woman","female","meditation","yoga","serenity","zen","mindfulness"],char:"\u{1f9d8}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},man_in_lotus_position:{keywords:["man","male","meditation","yoga","serenity","zen","mindfulness"],char:"\u{1f9d8}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"activity"},surfing_woman:{keywords:["sports","ocean","sea","summer","beach","woman","female"],char:"\u{1f3c4}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},surfing_man:{keywords:["sports","ocean","sea","summer","beach"],char:"\u{1f3c4}",fitzpatrick_scale:!0,category:"activity"},bath:{keywords:["clean","shower","bathroom"],char:"\u{1f6c0}",fitzpatrick_scale:!0,category:"activity"},basketball_woman:{keywords:["sports","human","woman","female"],char:"\u26f9\ufe0f\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},basketball_man:{keywords:["sports","human"],char:"\u26f9",fitzpatrick_scale:!0,category:"activity"},weight_lifting_woman:{keywords:["sports","training","exercise","woman","female"],char:"\u{1f3cb}\ufe0f\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},weight_lifting_man:{keywords:["sports","training","exercise"],char:"\u{1f3cb}",fitzpatrick_scale:!0,category:"activity"},biking_woman:{keywords:["sports","bike","exercise","hipster","woman","female"],char:"\u{1f6b4}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},biking_man:{keywords:["sports","bike","exercise","hipster"],char:"\u{1f6b4}",fitzpatrick_scale:!0,category:"activity"},mountain_biking_woman:{keywords:["transportation","sports","human","race","bike","woman","female"],char:"\u{1f6b5}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},mountain_biking_man:{keywords:["transportation","sports","human","race","bike"],char:"\u{1f6b5}",fitzpatrick_scale:!0,category:"activity"},horse_racing:{keywords:["animal","betting","competition","gambling","luck"],char:"\u{1f3c7}",fitzpatrick_scale:!0,category:"activity"},business_suit_levitating:{keywords:["suit","business","levitate","hover","jump"],char:"\u{1f574}",fitzpatrick_scale:!0,category:"activity"},trophy:{keywords:["win","award","contest","place","ftw","ceremony"],char:"\u{1f3c6}",fitzpatrick_scale:!1,category:"activity"},running_shirt_with_sash:{keywords:["play","pageant"],char:"\u{1f3bd}",fitzpatrick_scale:!1,category:"activity"},medal_sports:{keywords:["award","winning"],char:"\u{1f3c5}",fitzpatrick_scale:!1,category:"activity"},medal_military:{keywords:["award","winning","army"],char:"\u{1f396}",fitzpatrick_scale:!1,category:"activity"},"1st_place_medal":{keywords:["award","winning","first"],char:"\u{1f947}",fitzpatrick_scale:!1,category:"activity"},"2nd_place_medal":{keywords:["award","second"],char:"\u{1f948}",fitzpatrick_scale:!1,category:"activity"},"3rd_place_medal":{keywords:["award","third"],char:"\u{1f949}",fitzpatrick_scale:!1,category:"activity"},reminder_ribbon:{keywords:["sports","cause","support","awareness"],char:"\u{1f397}",fitzpatrick_scale:!1,category:"activity"},rosette:{keywords:["flower","decoration","military"],char:"\u{1f3f5}",fitzpatrick_scale:!1,category:"activity"},ticket:{keywords:["event","concert","pass"],char:"\u{1f3ab}",fitzpatrick_scale:!1,category:"activity"},tickets:{keywords:["sports","concert","entrance"],char:"\u{1f39f}",fitzpatrick_scale:!1,category:"activity"},performing_arts:{keywords:["acting","theater","drama"],char:"\u{1f3ad}",fitzpatrick_scale:!1,category:"activity"},art:{keywords:["design","paint","draw","colors"],char:"\u{1f3a8}",fitzpatrick_scale:!1,category:"activity"},circus_tent:{keywords:["festival","carnival","party"],char:"\u{1f3aa}",fitzpatrick_scale:!1,category:"activity"},woman_juggling:{keywords:["juggle","balance","skill","multitask"],char:"\u{1f939}\u200d\u2640\ufe0f",fitzpatrick_scale:!0,category:"activity"},man_juggling:{keywords:["juggle","balance","skill","multitask"],char:"\u{1f939}\u200d\u2642\ufe0f",fitzpatrick_scale:!0,category:"activity"},microphone:{keywords:["sound","music","PA","sing","talkshow"],char:"\u{1f3a4}",fitzpatrick_scale:!1,category:"activity"},headphones:{keywords:["music","score","gadgets"],char:"\u{1f3a7}",fitzpatrick_scale:!1,category:"activity"},musical_score:{keywords:["treble","clef","compose"],char:"\u{1f3bc}",fitzpatrick_scale:!1,category:"activity"},musical_keyboard:{keywords:["piano","instrument","compose"],char:"\u{1f3b9}",fitzpatrick_scale:!1,category:"activity"},drum:{keywords:["music","instrument","drumsticks","snare"],char:"\u{1f941}",fitzpatrick_scale:!1,category:"activity"},saxophone:{keywords:["music","instrument","jazz","blues"],char:"\u{1f3b7}",fitzpatrick_scale:!1,category:"activity"},trumpet:{keywords:["music","brass"],char:"\u{1f3ba}",fitzpatrick_scale:!1,category:"activity"},guitar:{keywords:["music","instrument"],char:"\u{1f3b8}",fitzpatrick_scale:!1,category:"activity"},violin:{keywords:["music","instrument","orchestra","symphony"],char:"\u{1f3bb}",fitzpatrick_scale:!1,category:"activity"},clapper:{keywords:["movie","film","record"],char:"\u{1f3ac}",fitzpatrick_scale:!1,category:"activity"},video_game:{keywords:["play","console","PS4","controller"],char:"\u{1f3ae}",fitzpatrick_scale:!1,category:"activity"},space_invader:{keywords:["game","arcade","play"],char:"\u{1f47e}",fitzpatrick_scale:!1,category:"activity"},dart:{keywords:["game","play","bar","target","bullseye"],char:"\u{1f3af}",fitzpatrick_scale:!1,category:"activity"},game_die:{keywords:["dice","random","tabletop","play","luck"],char:"\u{1f3b2}",fitzpatrick_scale:!1,category:"activity"},chess_pawn:{keywords:["expendable"],char:"\u265f",fitzpatrick_scale:!1,category:"activity"},slot_machine:{keywords:["bet","gamble","vegas","fruit machine","luck","casino"],char:"\u{1f3b0}",fitzpatrick_scale:!1,category:"activity"},jigsaw:{keywords:["interlocking","puzzle","piece"],char:"\u{1f9e9}",fitzpatrick_scale:!1,category:"activity"},bowling:{keywords:["sports","fun","play"],char:"\u{1f3b3}",fitzpatrick_scale:!1,category:"activity"},red_car:{keywords:["red","transportation","vehicle"],char:"\u{1f697}",fitzpatrick_scale:!1,category:"travel_and_places"},taxi:{keywords:["uber","vehicle","cars","transportation"],char:"\u{1f695}",fitzpatrick_scale:!1,category:"travel_and_places"},blue_car:{keywords:["transportation","vehicle"],char:"\u{1f699}",fitzpatrick_scale:!1,category:"travel_and_places"},bus:{keywords:["car","vehicle","transportation"],char:"\u{1f68c}",fitzpatrick_scale:!1,category:"travel_and_places"},trolleybus:{keywords:["bart","transportation","vehicle"],char:"\u{1f68e}",fitzpatrick_scale:!1,category:"travel_and_places"},racing_car:{keywords:["sports","race","fast","formula","f1"],char:"\u{1f3ce}",fitzpatrick_scale:!1,category:"travel_and_places"},police_car:{keywords:["vehicle","cars","transportation","law","legal","enforcement"],char:"\u{1f693}",fitzpatrick_scale:!1,category:"travel_and_places"},ambulance:{keywords:["health","911","hospital"],char:"\u{1f691}",fitzpatrick_scale:!1,category:"travel_and_places"},fire_engine:{keywords:["transportation","cars","vehicle"],char:"\u{1f692}",fitzpatrick_scale:!1,category:"travel_and_places"},minibus:{keywords:["vehicle","car","transportation"],char:"\u{1f690}",fitzpatrick_scale:!1,category:"travel_and_places"},truck:{keywords:["cars","transportation"],char:"\u{1f69a}",fitzpatrick_scale:!1,category:"travel_and_places"},articulated_lorry:{keywords:["vehicle","cars","transportation","express"],char:"\u{1f69b}",fitzpatrick_scale:!1,category:"travel_and_places"},tractor:{keywords:["vehicle","car","farming","agriculture"],char:"\u{1f69c}",fitzpatrick_scale:!1,category:"travel_and_places"},kick_scooter:{keywords:["vehicle","kick","razor"],char:"\u{1f6f4}",fitzpatrick_scale:!1,category:"travel_and_places"},motorcycle:{keywords:["race","sports","fast"],char:"\u{1f3cd}",fitzpatrick_scale:!1,category:"travel_and_places"},bike:{keywords:["sports","bicycle","exercise","hipster"],char:"\u{1f6b2}",fitzpatrick_scale:!1,category:"travel_and_places"},motor_scooter:{keywords:["vehicle","vespa","sasha"],char:"\u{1f6f5}",fitzpatrick_scale:!1,category:"travel_and_places"},rotating_light:{keywords:["police","ambulance","911","emergency","alert","error","pinged","law","legal"],char:"\u{1f6a8}",fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_police_car:{keywords:["vehicle","law","legal","enforcement","911"],char:"\u{1f694}",fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_bus:{keywords:["vehicle","transportation"],char:"\u{1f68d}",fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_automobile:{keywords:["car","vehicle","transportation"],char:"\u{1f698}",fitzpatrick_scale:!1,category:"travel_and_places"},oncoming_taxi:{keywords:["vehicle","cars","uber"],char:"\u{1f696}",fitzpatrick_scale:!1,category:"travel_and_places"},aerial_tramway:{keywords:["transportation","vehicle","ski"],char:"\u{1f6a1}",fitzpatrick_scale:!1,category:"travel_and_places"},mountain_cableway:{keywords:["transportation","vehicle","ski"],char:"\u{1f6a0}",fitzpatrick_scale:!1,category:"travel_and_places"},suspension_railway:{keywords:["vehicle","transportation"],char:"\u{1f69f}",fitzpatrick_scale:!1,category:"travel_and_places"},railway_car:{keywords:["transportation","vehicle"],char:"\u{1f683}",fitzpatrick_scale:!1,category:"travel_and_places"},train:{keywords:["transportation","vehicle","carriage","public","travel"],char:"\u{1f68b}",fitzpatrick_scale:!1,category:"travel_and_places"},monorail:{keywords:["transportation","vehicle"],char:"\u{1f69d}",fitzpatrick_scale:!1,category:"travel_and_places"},bullettrain_side:{keywords:["transportation","vehicle"],char:"\u{1f684}",fitzpatrick_scale:!1,category:"travel_and_places"},bullettrain_front:{keywords:["transportation","vehicle","speed","fast","public","travel"],char:"\u{1f685}",fitzpatrick_scale:!1,category:"travel_and_places"},light_rail:{keywords:["transportation","vehicle"],char:"\u{1f688}",fitzpatrick_scale:!1,category:"travel_and_places"},mountain_railway:{keywords:["transportation","vehicle"],char:"\u{1f69e}",fitzpatrick_scale:!1,category:"travel_and_places"},steam_locomotive:{keywords:["transportation","vehicle","train"],char:"\u{1f682}",fitzpatrick_scale:!1,category:"travel_and_places"},train2:{keywords:["transportation","vehicle"],char:"\u{1f686}",fitzpatrick_scale:!1,category:"travel_and_places"},metro:{keywords:["transportation","blue-square","mrt","underground","tube"],char:"\u{1f687}",fitzpatrick_scale:!1,category:"travel_and_places"},tram:{keywords:["transportation","vehicle"],char:"\u{1f68a}",fitzpatrick_scale:!1,category:"travel_and_places"},station:{keywords:["transportation","vehicle","public"],char:"\u{1f689}",fitzpatrick_scale:!1,category:"travel_and_places"},flying_saucer:{keywords:["transportation","vehicle","ufo"],char:"\u{1f6f8}",fitzpatrick_scale:!1,category:"travel_and_places"},helicopter:{keywords:["transportation","vehicle","fly"],char:"\u{1f681}",fitzpatrick_scale:!1,category:"travel_and_places"},small_airplane:{keywords:["flight","transportation","fly","vehicle"],char:"\u{1f6e9}",fitzpatrick_scale:!1,category:"travel_and_places"},airplane:{keywords:["vehicle","transportation","flight","fly"],char:"\u2708\ufe0f",fitzpatrick_scale:!1,category:"travel_and_places"},flight_departure:{keywords:["airport","flight","landing"],char:"\u{1f6eb}",fitzpatrick_scale:!1,category:"travel_and_places"},flight_arrival:{keywords:["airport","flight","boarding"],char:"\u{1f6ec}",fitzpatrick_scale:!1,category:"travel_and_places"},sailboat:{keywords:["ship","summer","transportation","water","sailing"],char:"\u26f5",fitzpatrick_scale:!1,category:"travel_and_places"},motor_boat:{keywords:["ship"],char:"\u{1f6e5}",fitzpatrick_scale:!1,category:"travel_and_places"},speedboat:{keywords:["ship","transportation","vehicle","summer"],char:"\u{1f6a4}",fitzpatrick_scale:!1,category:"travel_and_places"},ferry:{keywords:["boat","ship","yacht"],char:"\u26f4",fitzpatrick_scale:!1,category:"travel_and_places"},passenger_ship:{keywords:["yacht","cruise","ferry"],char:"\u{1f6f3}",fitzpatrick_scale:!1,category:"travel_and_places"},rocket:{keywords:["launch","ship","staffmode","NASA","outer space","outer_space","fly"],char:"\u{1f680}",fitzpatrick_scale:!1,category:"travel_and_places"},artificial_satellite:{keywords:["communication","gps","orbit","spaceflight","NASA","ISS"],char:"\u{1f6f0}",fitzpatrick_scale:!1,category:"travel_and_places"},seat:{keywords:["sit","airplane","transport","bus","flight","fly"],char:"\u{1f4ba}",fitzpatrick_scale:!1,category:"travel_and_places"},canoe:{keywords:["boat","paddle","water","ship"],char:"\u{1f6f6}",fitzpatrick_scale:!1,category:"travel_and_places"},anchor:{keywords:["ship","ferry","sea","boat"],char:"\u2693",fitzpatrick_scale:!1,category:"travel_and_places"},construction:{keywords:["wip","progress","caution","warning"],char:"\u{1f6a7}",fitzpatrick_scale:!1,category:"travel_and_places"},fuelpump:{keywords:["gas station","petroleum"],char:"\u26fd",fitzpatrick_scale:!1,category:"travel_and_places"},busstop:{keywords:["transportation","wait"],char:"\u{1f68f}",fitzpatrick_scale:!1,category:"travel_and_places"},vertical_traffic_light:{keywords:["transportation","driving"],char:"\u{1f6a6}",fitzpatrick_scale:!1,category:"travel_and_places"},traffic_light:{keywords:["transportation","signal"],char:"\u{1f6a5}",fitzpatrick_scale:!1,category:"travel_and_places"},checkered_flag:{keywords:["contest","finishline","race","gokart"],char:"\u{1f3c1}",fitzpatrick_scale:!1,category:"travel_and_places"},ship:{keywords:["transportation","titanic","deploy"],char:"\u{1f6a2}",fitzpatrick_scale:!1,category:"travel_and_places"},ferris_wheel:{keywords:["photo","carnival","londoneye"],char:"\u{1f3a1}",fitzpatrick_scale:!1,category:"travel_and_places"},roller_coaster:{keywords:["carnival","playground","photo","fun"],char:"\u{1f3a2}",fitzpatrick_scale:!1,category:"travel_and_places"},carousel_horse:{keywords:["photo","carnival"],char:"\u{1f3a0}",fitzpatrick_scale:!1,category:"travel_and_places"},building_construction:{keywords:["wip","working","progress"],char:"\u{1f3d7}",fitzpatrick_scale:!1,category:"travel_and_places"},foggy:{keywords:["photo","mountain"],char:"\u{1f301}",fitzpatrick_scale:!1,category:"travel_and_places"},tokyo_tower:{keywords:["photo","japanese"],char:"\u{1f5fc}",fitzpatrick_scale:!1,category:"travel_and_places"},factory:{keywords:["building","industry","pollution","smoke"],char:"\u{1f3ed}",fitzpatrick_scale:!1,category:"travel_and_places"},fountain:{keywords:["photo","summer","water","fresh"],char:"\u26f2",fitzpatrick_scale:!1,category:"travel_and_places"},rice_scene:{keywords:["photo","japan","asia","tsukimi"],char:"\u{1f391}",fitzpatrick_scale:!1,category:"travel_and_places"},mountain:{keywords:["photo","nature","environment"],char:"\u26f0",fitzpatrick_scale:!1,category:"travel_and_places"},mountain_snow:{keywords:["photo","nature","environment","winter","cold"],char:"\u{1f3d4}",fitzpatrick_scale:!1,category:"travel_and_places"},mount_fuji:{keywords:["photo","mountain","nature","japanese"],char:"\u{1f5fb}",fitzpatrick_scale:!1,category:"travel_and_places"},volcano:{keywords:["photo","nature","disaster"],char:"\u{1f30b}",fitzpatrick_scale:!1,category:"travel_and_places"},japan:{keywords:["nation","country","japanese","asia"],char:"\u{1f5fe}",fitzpatrick_scale:!1,category:"travel_and_places"},camping:{keywords:["photo","outdoors","tent"],char:"\u{1f3d5}",fitzpatrick_scale:!1,category:"travel_and_places"},tent:{keywords:["photo","camping","outdoors"],char:"\u26fa",fitzpatrick_scale:!1,category:"travel_and_places"},national_park:{keywords:["photo","environment","nature"],char:"\u{1f3de}",fitzpatrick_scale:!1,category:"travel_and_places"},motorway:{keywords:["road","cupertino","interstate","highway"],char:"\u{1f6e3}",fitzpatrick_scale:!1,category:"travel_and_places"},railway_track:{keywords:["train","transportation"],char:"\u{1f6e4}",fitzpatrick_scale:!1,category:"travel_and_places"},sunrise:{keywords:["morning","view","vacation","photo"],char:"\u{1f305}",fitzpatrick_scale:!1,category:"travel_and_places"},sunrise_over_mountains:{keywords:["view","vacation","photo"],char:"\u{1f304}",fitzpatrick_scale:!1,category:"travel_and_places"},desert:{keywords:["photo","warm","saharah"],char:"\u{1f3dc}",fitzpatrick_scale:!1,category:"travel_and_places"},beach_umbrella:{keywords:["weather","summer","sunny","sand","mojito"],char:"\u{1f3d6}",fitzpatrick_scale:!1,category:"travel_and_places"},desert_island:{keywords:["photo","tropical","mojito"],char:"\u{1f3dd}",fitzpatrick_scale:!1,category:"travel_and_places"},city_sunrise:{keywords:["photo","good morning","dawn"],char:"\u{1f307}",fitzpatrick_scale:!1,category:"travel_and_places"},city_sunset:{keywords:["photo","evening","sky","buildings"],char:"\u{1f306}",fitzpatrick_scale:!1,category:"travel_and_places"},cityscape:{keywords:["photo","night life","urban"],char:"\u{1f3d9}",fitzpatrick_scale:!1,category:"travel_and_places"},night_with_stars:{keywords:["evening","city","downtown"],char:"\u{1f303}",fitzpatrick_scale:!1,category:"travel_and_places"},bridge_at_night:{keywords:["photo","sanfrancisco"],char:"\u{1f309}",fitzpatrick_scale:!1,category:"travel_and_places"},milky_way:{keywords:["photo","space","stars"],char:"\u{1f30c}",fitzpatrick_scale:!1,category:"travel_and_places"},stars:{keywords:["night","photo"],char:"\u{1f320}",fitzpatrick_scale:!1,category:"travel_and_places"},sparkler:{keywords:["stars","night","shine"],char:"\u{1f387}",fitzpatrick_scale:!1,category:"travel_and_places"},fireworks:{keywords:["photo","festival","carnival","congratulations"],char:"\u{1f386}",fitzpatrick_scale:!1,category:"travel_and_places"},rainbow:{keywords:["nature","happy","unicorn_face","photo","sky","spring"],char:"\u{1f308}",fitzpatrick_scale:!1,category:"travel_and_places"},houses:{keywords:["buildings","photo"],char:"\u{1f3d8}",fitzpatrick_scale:!1,category:"travel_and_places"},european_castle:{keywords:["building","royalty","history"],char:"\u{1f3f0}",fitzpatrick_scale:!1,category:"travel_and_places"},japanese_castle:{keywords:["photo","building"],char:"\u{1f3ef}",fitzpatrick_scale:!1,category:"travel_and_places"},stadium:{keywords:["photo","place","sports","concert","venue"],char:"\u{1f3df}",fitzpatrick_scale:!1,category:"travel_and_places"},statue_of_liberty:{keywords:["american","newyork"],char:"\u{1f5fd}",fitzpatrick_scale:!1,category:"travel_and_places"},house:{keywords:["building","home"],char:"\u{1f3e0}",fitzpatrick_scale:!1,category:"travel_and_places"},house_with_garden:{keywords:["home","plant","nature"],char:"\u{1f3e1}",fitzpatrick_scale:!1,category:"travel_and_places"},derelict_house:{keywords:["abandon","evict","broken","building"],char:"\u{1f3da}",fitzpatrick_scale:!1,category:"travel_and_places"},office:{keywords:["building","bureau","work"],char:"\u{1f3e2}",fitzpatrick_scale:!1,category:"travel_and_places"},department_store:{keywords:["building","shopping","mall"],char:"\u{1f3ec}",fitzpatrick_scale:!1,category:"travel_and_places"},post_office:{keywords:["building","envelope","communication"],char:"\u{1f3e3}",fitzpatrick_scale:!1,category:"travel_and_places"},european_post_office:{keywords:["building","email"],char:"\u{1f3e4}",fitzpatrick_scale:!1,category:"travel_and_places"},hospital:{keywords:["building","health","surgery","doctor"],char:"\u{1f3e5}",fitzpatrick_scale:!1,category:"travel_and_places"},bank:{keywords:["building","money","sales","cash","business","enterprise"],char:"\u{1f3e6}",fitzpatrick_scale:!1,category:"travel_and_places"},hotel:{keywords:["building","accomodation","checkin"],char:"\u{1f3e8}",fitzpatrick_scale:!1,category:"travel_and_places"},convenience_store:{keywords:["building","shopping","groceries"],char:"\u{1f3ea}",fitzpatrick_scale:!1,category:"travel_and_places"},school:{keywords:["building","student","education","learn","teach"],char:"\u{1f3eb}",fitzpatrick_scale:!1,category:"travel_and_places"},love_hotel:{keywords:["like","affection","dating"],char:"\u{1f3e9}",fitzpatrick_scale:!1,category:"travel_and_places"},wedding:{keywords:["love","like","affection","couple","marriage","bride","groom"],char:"\u{1f492}",fitzpatrick_scale:!1,category:"travel_and_places"},classical_building:{keywords:["art","culture","history"],char:"\u{1f3db}",fitzpatrick_scale:!1,category:"travel_and_places"},church:{keywords:["building","religion","christ"],char:"\u26ea",fitzpatrick_scale:!1,category:"travel_and_places"},mosque:{keywords:["islam","worship","minaret"],char:"\u{1f54c}",fitzpatrick_scale:!1,category:"travel_and_places"},synagogue:{keywords:["judaism","worship","temple","jewish"],char:"\u{1f54d}",fitzpatrick_scale:!1,category:"travel_and_places"},kaaba:{keywords:["mecca","mosque","islam"],char:"\u{1f54b}",fitzpatrick_scale:!1,category:"travel_and_places"},shinto_shrine:{keywords:["temple","japan","kyoto"],char:"\u26e9",fitzpatrick_scale:!1,category:"travel_and_places"},watch:{keywords:["time","accessories"],char:"\u231a",fitzpatrick_scale:!1,category:"objects"},iphone:{keywords:["technology","apple","gadgets","dial"],char:"\u{1f4f1}",fitzpatrick_scale:!1,category:"objects"},calling:{keywords:["iphone","incoming"],char:"\u{1f4f2}",fitzpatrick_scale:!1,category:"objects"},computer:{keywords:["technology","laptop","screen","display","monitor"],char:"\u{1f4bb}",fitzpatrick_scale:!1,category:"objects"},keyboard:{keywords:["technology","computer","type","input","text"],char:"\u2328",fitzpatrick_scale:!1,category:"objects"},desktop_computer:{keywords:["technology","computing","screen"],char:"\u{1f5a5}",fitzpatrick_scale:!1,category:"objects"},printer:{keywords:["paper","ink"],char:"\u{1f5a8}",fitzpatrick_scale:!1,category:"objects"},computer_mouse:{keywords:["click"],char:"\u{1f5b1}",fitzpatrick_scale:!1,category:"objects"},trackball:{keywords:["technology","trackpad"],char:"\u{1f5b2}",fitzpatrick_scale:!1,category:"objects"},joystick:{keywords:["game","play"],char:"\u{1f579}",fitzpatrick_scale:!1,category:"objects"},clamp:{keywords:["tool"],char:"\u{1f5dc}",fitzpatrick_scale:!1,category:"objects"},minidisc:{keywords:["technology","record","data","disk","90s"],char:"\u{1f4bd}",fitzpatrick_scale:!1,category:"objects"},floppy_disk:{keywords:["oldschool","technology","save","90s","80s"],char:"\u{1f4be}",fitzpatrick_scale:!1,category:"objects"},cd:{keywords:["technology","dvd","disk","disc","90s"],char:"\u{1f4bf}",fitzpatrick_scale:!1,category:"objects"},dvd:{keywords:["cd","disk","disc"],char:"\u{1f4c0}",fitzpatrick_scale:!1,category:"objects"},vhs:{keywords:["record","video","oldschool","90s","80s"],char:"\u{1f4fc}",fitzpatrick_scale:!1,category:"objects"},camera:{keywords:["gadgets","photography"],char:"\u{1f4f7}",fitzpatrick_scale:!1,category:"objects"},camera_flash:{keywords:["photography","gadgets"],char:"\u{1f4f8}",fitzpatrick_scale:!1,category:"objects"},video_camera:{keywords:["film","record"],char:"\u{1f4f9}",fitzpatrick_scale:!1,category:"objects"},movie_camera:{keywords:["film","record"],char:"\u{1f3a5}",fitzpatrick_scale:!1,category:"objects"},film_projector:{keywords:["video","tape","record","movie"],char:"\u{1f4fd}",fitzpatrick_scale:!1,category:"objects"},film_strip:{keywords:["movie"],char:"\u{1f39e}",fitzpatrick_scale:!1,category:"objects"},telephone_receiver:{keywords:["technology","communication","dial"],char:"\u{1f4de}",fitzpatrick_scale:!1,category:"objects"},phone:{keywords:["technology","communication","dial","telephone"],char:"\u260e\ufe0f",fitzpatrick_scale:!1,category:"objects"},pager:{keywords:["bbcall","oldschool","90s"],char:"\u{1f4df}",fitzpatrick_scale:!1,category:"objects"},fax:{keywords:["communication","technology"],char:"\u{1f4e0}",fitzpatrick_scale:!1,category:"objects"},tv:{keywords:["technology","program","oldschool","show","television"],char:"\u{1f4fa}",fitzpatrick_scale:!1,category:"objects"},radio:{keywords:["communication","music","podcast","program"],char:"\u{1f4fb}",fitzpatrick_scale:!1,category:"objects"},studio_microphone:{keywords:["sing","recording","artist","talkshow"],char:"\u{1f399}",fitzpatrick_scale:!1,category:"objects"},level_slider:{keywords:["scale"],char:"\u{1f39a}",fitzpatrick_scale:!1,category:"objects"},control_knobs:{keywords:["dial"],char:"\u{1f39b}",fitzpatrick_scale:!1,category:"objects"},compass:{keywords:["magnetic","navigation","orienteering"],char:"\u{1f9ed}",fitzpatrick_scale:!1,category:"objects"},stopwatch:{keywords:["time","deadline"],char:"\u23f1",fitzpatrick_scale:!1,category:"objects"},timer_clock:{keywords:["alarm"],char:"\u23f2",fitzpatrick_scale:!1,category:"objects"},alarm_clock:{keywords:["time","wake"],char:"\u23f0",fitzpatrick_scale:!1,category:"objects"},mantelpiece_clock:{keywords:["time"],char:"\u{1f570}",fitzpatrick_scale:!1,category:"objects"},hourglass_flowing_sand:{keywords:["oldschool","time","countdown"],char:"\u23f3",fitzpatrick_scale:!1,category:"objects"},hourglass:{keywords:["time","clock","oldschool","limit","exam","quiz","test"],char:"\u231b",fitzpatrick_scale:!1,category:"objects"},satellite:{keywords:["communication","future","radio","space"],char:"\u{1f4e1}",fitzpatrick_scale:!1,category:"objects"},battery:{keywords:["power","energy","sustain"],char:"\u{1f50b}",fitzpatrick_scale:!1,category:"objects"},electric_plug:{keywords:["charger","power"],char:"\u{1f50c}",fitzpatrick_scale:!1,category:"objects"},bulb:{keywords:["light","electricity","idea"],char:"\u{1f4a1}",fitzpatrick_scale:!1,category:"objects"},flashlight:{keywords:["dark","camping","sight","night"],char:"\u{1f526}",fitzpatrick_scale:!1,category:"objects"},candle:{keywords:["fire","wax"],char:"\u{1f56f}",fitzpatrick_scale:!1,category:"objects"},fire_extinguisher:{keywords:["quench"],char:"\u{1f9ef}",fitzpatrick_scale:!1,category:"objects"},wastebasket:{keywords:["bin","trash","rubbish","garbage","toss"],char:"\u{1f5d1}",fitzpatrick_scale:!1,category:"objects"},oil_drum:{keywords:["barrell"],char:"\u{1f6e2}",fitzpatrick_scale:!1,category:"objects"},money_with_wings:{keywords:["dollar","bills","payment","sale"],char:"\u{1f4b8}",fitzpatrick_scale:!1,category:"objects"},dollar:{keywords:["money","sales","bill","currency"],char:"\u{1f4b5}",fitzpatrick_scale:!1,category:"objects"},yen:{keywords:["money","sales","japanese","dollar","currency"],char:"\u{1f4b4}",fitzpatrick_scale:!1,category:"objects"},euro:{keywords:["money","sales","dollar","currency"],char:"\u{1f4b6}",fitzpatrick_scale:!1,category:"objects"},pound:{keywords:["british","sterling","money","sales","bills","uk","england","currency"],char:"\u{1f4b7}",fitzpatrick_scale:!1,category:"objects"},moneybag:{keywords:["dollar","payment","coins","sale"],char:"\u{1f4b0}",fitzpatrick_scale:!1,category:"objects"},credit_card:{keywords:["money","sales","dollar","bill","payment","shopping"],char:"\u{1f4b3}",fitzpatrick_scale:!1,category:"objects"},gem:{keywords:["blue","ruby","diamond","jewelry"],char:"\u{1f48e}",fitzpatrick_scale:!1,category:"objects"},balance_scale:{keywords:["law","fairness","weight"],char:"\u2696",fitzpatrick_scale:!1,category:"objects"},toolbox:{keywords:["tools","diy","fix","maintainer","mechanic"],char:"\u{1f9f0}",fitzpatrick_scale:!1,category:"objects"},wrench:{keywords:["tools","diy","ikea","fix","maintainer"],char:"\u{1f527}",fitzpatrick_scale:!1,category:"objects"},hammer:{keywords:["tools","build","create"],char:"\u{1f528}",fitzpatrick_scale:!1,category:"objects"},hammer_and_pick:{keywords:["tools","build","create"],char:"\u2692",fitzpatrick_scale:!1,category:"objects"},hammer_and_wrench:{keywords:["tools","build","create"],char:"\u{1f6e0}",fitzpatrick_scale:!1,category:"objects"},pick:{keywords:["tools","dig"],char:"\u26cf",fitzpatrick_scale:!1,category:"objects"},nut_and_bolt:{keywords:["handy","tools","fix"],char:"\u{1f529}",fitzpatrick_scale:!1,category:"objects"},gear:{keywords:["cog"],char:"\u2699",fitzpatrick_scale:!1,category:"objects"},brick:{keywords:["bricks"],char:"\u{1f9f1}",fitzpatrick_scale:!1,category:"objects"},chains:{keywords:["lock","arrest"],char:"\u26d3",fitzpatrick_scale:!1,category:"objects"},magnet:{keywords:["attraction","magnetic"],char:"\u{1f9f2}",fitzpatrick_scale:!1,category:"objects"},gun:{keywords:["violence","weapon","pistol","revolver"],char:"\u{1f52b}",fitzpatrick_scale:!1,category:"objects"},bomb:{keywords:["boom","explode","explosion","terrorism"],char:"\u{1f4a3}",fitzpatrick_scale:!1,category:"objects"},firecracker:{keywords:["dynamite","boom","explode","explosion","explosive"],char:"\u{1f9e8}",fitzpatrick_scale:!1,category:"objects"},hocho:{keywords:["knife","blade","cutlery","kitchen","weapon"],char:"\u{1f52a}",fitzpatrick_scale:!1,category:"objects"},dagger:{keywords:["weapon"],char:"\u{1f5e1}",fitzpatrick_scale:!1,category:"objects"},crossed_swords:{keywords:["weapon"],char:"\u2694",fitzpatrick_scale:!1,category:"objects"},shield:{keywords:["protection","security"],char:"\u{1f6e1}",fitzpatrick_scale:!1,category:"objects"},smoking:{keywords:["kills","tobacco","cigarette","joint","smoke"],char:"\u{1f6ac}",fitzpatrick_scale:!1,category:"objects"},skull_and_crossbones:{keywords:["poison","danger","deadly","scary","death","pirate","evil"],char:"\u2620",fitzpatrick_scale:!1,category:"objects"},coffin:{keywords:["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],char:"\u26b0",fitzpatrick_scale:!1,category:"objects"},funeral_urn:{keywords:["dead","die","death","rip","ashes"],char:"\u26b1",fitzpatrick_scale:!1,category:"objects"},amphora:{keywords:["vase","jar"],char:"\u{1f3fa}",fitzpatrick_scale:!1,category:"objects"},crystal_ball:{keywords:["disco","party","magic","circus","fortune_teller"],char:"\u{1f52e}",fitzpatrick_scale:!1,category:"objects"},prayer_beads:{keywords:["dhikr","religious"],char:"\u{1f4ff}",fitzpatrick_scale:!1,category:"objects"},nazar_amulet:{keywords:["bead","charm"],char:"\u{1f9ff}",fitzpatrick_scale:!1,category:"objects"},barber:{keywords:["hair","salon","style"],char:"\u{1f488}",fitzpatrick_scale:!1,category:"objects"},alembic:{keywords:["distilling","science","experiment","chemistry"],char:"\u2697",fitzpatrick_scale:!1,category:"objects"},telescope:{keywords:["stars","space","zoom","science","astronomy"],char:"\u{1f52d}",fitzpatrick_scale:!1,category:"objects"},microscope:{keywords:["laboratory","experiment","zoomin","science","study"],char:"\u{1f52c}",fitzpatrick_scale:!1,category:"objects"},hole:{keywords:["embarrassing"],char:"\u{1f573}",fitzpatrick_scale:!1,category:"objects"},pill:{keywords:["health","medicine","doctor","pharmacy","drug"],char:"\u{1f48a}",fitzpatrick_scale:!1,category:"objects"},syringe:{keywords:["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],char:"\u{1f489}",fitzpatrick_scale:!1,category:"objects"},dna:{keywords:["biologist","genetics","life"],char:"\u{1f9ec}",fitzpatrick_scale:!1,category:"objects"},microbe:{keywords:["amoeba","bacteria","germs"],char:"\u{1f9a0}",fitzpatrick_scale:!1,category:"objects"},petri_dish:{keywords:["bacteria","biology","culture","lab"],char:"\u{1f9eb}",fitzpatrick_scale:!1,category:"objects"},test_tube:{keywords:["chemistry","experiment","lab","science"],char:"\u{1f9ea}",fitzpatrick_scale:!1,category:"objects"},thermometer:{keywords:["weather","temperature","hot","cold"],char:"\u{1f321}",fitzpatrick_scale:!1,category:"objects"},broom:{keywords:["cleaning","sweeping","witch"],char:"\u{1f9f9}",fitzpatrick_scale:!1,category:"objects"},basket:{keywords:["laundry"],char:"\u{1f9fa}",fitzpatrick_scale:!1,category:"objects"},toilet_paper:{keywords:["roll"],char:"\u{1f9fb}",fitzpatrick_scale:!1,category:"objects"},label:{keywords:["sale","tag"],char:"\u{1f3f7}",fitzpatrick_scale:!1,category:"objects"},bookmark:{keywords:["favorite","label","save"],char:"\u{1f516}",fitzpatrick_scale:!1,category:"objects"},toilet:{keywords:["restroom","wc","washroom","bathroom","potty"],char:"\u{1f6bd}",fitzpatrick_scale:!1,category:"objects"},shower:{keywords:["clean","water","bathroom"],char:"\u{1f6bf}",fitzpatrick_scale:!1,category:"objects"},bathtub:{keywords:["clean","shower","bathroom"],char:"\u{1f6c1}",fitzpatrick_scale:!1,category:"objects"},soap:{keywords:["bar","bathing","cleaning","lather"],char:"\u{1f9fc}",fitzpatrick_scale:!1,category:"objects"},sponge:{keywords:["absorbing","cleaning","porous"],char:"\u{1f9fd}",fitzpatrick_scale:!1,category:"objects"},lotion_bottle:{keywords:["moisturizer","sunscreen"],char:"\u{1f9f4}",fitzpatrick_scale:!1,category:"objects"},key:{keywords:["lock","door","password"],char:"\u{1f511}",fitzpatrick_scale:!1,category:"objects"},old_key:{keywords:["lock","door","password"],char:"\u{1f5dd}",fitzpatrick_scale:!1,category:"objects"},couch_and_lamp:{keywords:["read","chill"],char:"\u{1f6cb}",fitzpatrick_scale:!1,category:"objects"},sleeping_bed:{keywords:["bed","rest"],char:"\u{1f6cc}",fitzpatrick_scale:!0,category:"objects"},bed:{keywords:["sleep","rest"],char:"\u{1f6cf}",fitzpatrick_scale:!1,category:"objects"},door:{keywords:["house","entry","exit"],char:"\u{1f6aa}",fitzpatrick_scale:!1,category:"objects"},bellhop_bell:{keywords:["service"],char:"\u{1f6ce}",fitzpatrick_scale:!1,category:"objects"},teddy_bear:{keywords:["plush","stuffed"],char:"\u{1f9f8}",fitzpatrick_scale:!1,category:"objects"},framed_picture:{keywords:["photography"],char:"\u{1f5bc}",fitzpatrick_scale:!1,category:"objects"},world_map:{keywords:["location","direction"],char:"\u{1f5fa}",fitzpatrick_scale:!1,category:"objects"},parasol_on_ground:{keywords:["weather","summer"],char:"\u26f1",fitzpatrick_scale:!1,category:"objects"},moyai:{keywords:["rock","easter island","moai"],char:"\u{1f5ff}",fitzpatrick_scale:!1,category:"objects"},shopping:{keywords:["mall","buy","purchase"],char:"\u{1f6cd}",fitzpatrick_scale:!1,category:"objects"},shopping_cart:{keywords:["trolley"],char:"\u{1f6d2}",fitzpatrick_scale:!1,category:"objects"},balloon:{keywords:["party","celebration","birthday","circus"],char:"\u{1f388}",fitzpatrick_scale:!1,category:"objects"},flags:{keywords:["fish","japanese","koinobori","carp","banner"],char:"\u{1f38f}",fitzpatrick_scale:!1,category:"objects"},ribbon:{keywords:["decoration","pink","girl","bowtie"],char:"\u{1f380}",fitzpatrick_scale:!1,category:"objects"},gift:{keywords:["present","birthday","christmas","xmas"],char:"\u{1f381}",fitzpatrick_scale:!1,category:"objects"},confetti_ball:{keywords:["festival","party","birthday","circus"],char:"\u{1f38a}",fitzpatrick_scale:!1,category:"objects"},tada:{keywords:["party","congratulations","birthday","magic","circus","celebration"],char:"\u{1f389}",fitzpatrick_scale:!1,category:"objects"},dolls:{keywords:["japanese","toy","kimono"],char:"\u{1f38e}",fitzpatrick_scale:!1,category:"objects"},wind_chime:{keywords:["nature","ding","spring","bell"],char:"\u{1f390}",fitzpatrick_scale:!1,category:"objects"},crossed_flags:{keywords:["japanese","nation","country","border"],char:"\u{1f38c}",fitzpatrick_scale:!1,category:"objects"},izakaya_lantern:{keywords:["light","paper","halloween","spooky"],char:"\u{1f3ee}",fitzpatrick_scale:!1,category:"objects"},red_envelope:{keywords:["gift"],char:"\u{1f9e7}",fitzpatrick_scale:!1,category:"objects"},email:{keywords:["letter","postal","inbox","communication"],char:"\u2709\ufe0f",fitzpatrick_scale:!1,category:"objects"},envelope_with_arrow:{keywords:["email","communication"],char:"\u{1f4e9}",fitzpatrick_scale:!1,category:"objects"},incoming_envelope:{keywords:["email","inbox"],char:"\u{1f4e8}",fitzpatrick_scale:!1,category:"objects"},"e-mail":{keywords:["communication","inbox"],char:"\u{1f4e7}",fitzpatrick_scale:!1,category:"objects"},love_letter:{keywords:["email","like","affection","envelope","valentines"],char:"\u{1f48c}",fitzpatrick_scale:!1,category:"objects"},postbox:{keywords:["email","letter","envelope"],char:"\u{1f4ee}",fitzpatrick_scale:!1,category:"objects"},mailbox_closed:{keywords:["email","communication","inbox"],char:"\u{1f4ea}",fitzpatrick_scale:!1,category:"objects"},mailbox:{keywords:["email","inbox","communication"],char:"\u{1f4eb}",fitzpatrick_scale:!1,category:"objects"},mailbox_with_mail:{keywords:["email","inbox","communication"],char:"\u{1f4ec}",fitzpatrick_scale:!1,category:"objects"},mailbox_with_no_mail:{keywords:["email","inbox"],char:"\u{1f4ed}",fitzpatrick_scale:!1,category:"objects"},package:{keywords:["mail","gift","cardboard","box","moving"],char:"\u{1f4e6}",fitzpatrick_scale:!1,category:"objects"},postal_horn:{keywords:["instrument","music"],char:"\u{1f4ef}",fitzpatrick_scale:!1,category:"objects"},inbox_tray:{keywords:["email","documents"],char:"\u{1f4e5}",fitzpatrick_scale:!1,category:"objects"},outbox_tray:{keywords:["inbox","email"],char:"\u{1f4e4}",fitzpatrick_scale:!1,category:"objects"},scroll:{keywords:["documents","ancient","history","paper"],char:"\u{1f4dc}",fitzpatrick_scale:!1,category:"objects"},page_with_curl:{keywords:["documents","office","paper"],char:"\u{1f4c3}",fitzpatrick_scale:!1,category:"objects"},bookmark_tabs:{keywords:["favorite","save","order","tidy"],char:"\u{1f4d1}",fitzpatrick_scale:!1,category:"objects"},receipt:{keywords:["accounting","expenses"],char:"\u{1f9fe}",fitzpatrick_scale:!1,category:"objects"},bar_chart:{keywords:["graph","presentation","stats"],char:"\u{1f4ca}",fitzpatrick_scale:!1,category:"objects"},chart_with_upwards_trend:{keywords:["graph","presentation","stats","recovery","business","economics","money","sales","good","success"],char:"\u{1f4c8}",fitzpatrick_scale:!1,category:"objects"},chart_with_downwards_trend:{keywords:["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],char:"\u{1f4c9}",fitzpatrick_scale:!1,category:"objects"},page_facing_up:{keywords:["documents","office","paper","information"],char:"\u{1f4c4}",fitzpatrick_scale:!1,category:"objects"},date:{keywords:["calendar","schedule"],char:"\u{1f4c5}",fitzpatrick_scale:!1,category:"objects"},calendar:{keywords:["schedule","date","planning"],char:"\u{1f4c6}",fitzpatrick_scale:!1,category:"objects"},spiral_calendar:{keywords:["date","schedule","planning"],char:"\u{1f5d3}",fitzpatrick_scale:!1,category:"objects"},card_index:{keywords:["business","stationery"],char:"\u{1f4c7}",fitzpatrick_scale:!1,category:"objects"},card_file_box:{keywords:["business","stationery"],char:"\u{1f5c3}",fitzpatrick_scale:!1,category:"objects"},ballot_box:{keywords:["election","vote"],char:"\u{1f5f3}",fitzpatrick_scale:!1,category:"objects"},file_cabinet:{keywords:["filing","organizing"],char:"\u{1f5c4}",fitzpatrick_scale:!1,category:"objects"},clipboard:{keywords:["stationery","documents"],char:"\u{1f4cb}",fitzpatrick_scale:!1,category:"objects"},spiral_notepad:{keywords:["memo","stationery"],char:"\u{1f5d2}",fitzpatrick_scale:!1,category:"objects"},file_folder:{keywords:["documents","business","office"],char:"\u{1f4c1}",fitzpatrick_scale:!1,category:"objects"},open_file_folder:{keywords:["documents","load"],char:"\u{1f4c2}",fitzpatrick_scale:!1,category:"objects"},card_index_dividers:{keywords:["organizing","business","stationery"],char:"\u{1f5c2}",fitzpatrick_scale:!1,category:"objects"},newspaper_roll:{keywords:["press","headline"],char:"\u{1f5de}",fitzpatrick_scale:!1,category:"objects"},newspaper:{keywords:["press","headline"],char:"\u{1f4f0}",fitzpatrick_scale:!1,category:"objects"},notebook:{keywords:["stationery","record","notes","paper","study"],char:"\u{1f4d3}",fitzpatrick_scale:!1,category:"objects"},closed_book:{keywords:["read","library","knowledge","textbook","learn"],char:"\u{1f4d5}",fitzpatrick_scale:!1,category:"objects"},green_book:{keywords:["read","library","knowledge","study"],char:"\u{1f4d7}",fitzpatrick_scale:!1,category:"objects"},blue_book:{keywords:["read","library","knowledge","learn","study"],char:"\u{1f4d8}",fitzpatrick_scale:!1,category:"objects"},orange_book:{keywords:["read","library","knowledge","textbook","study"],char:"\u{1f4d9}",fitzpatrick_scale:!1,category:"objects"},notebook_with_decorative_cover:{keywords:["classroom","notes","record","paper","study"],char:"\u{1f4d4}",fitzpatrick_scale:!1,category:"objects"},ledger:{keywords:["notes","paper"],char:"\u{1f4d2}",fitzpatrick_scale:!1,category:"objects"},books:{keywords:["literature","library","study"],char:"\u{1f4da}",fitzpatrick_scale:!1,category:"objects"},open_book:{keywords:["book","read","library","knowledge","literature","learn","study"],char:"\u{1f4d6}",fitzpatrick_scale:!1,category:"objects"},safety_pin:{keywords:["diaper"],char:"\u{1f9f7}",fitzpatrick_scale:!1,category:"objects"},link:{keywords:["rings","url"],char:"\u{1f517}",fitzpatrick_scale:!1,category:"objects"},paperclip:{keywords:["documents","stationery"],char:"\u{1f4ce}",fitzpatrick_scale:!1,category:"objects"},paperclips:{keywords:["documents","stationery"],char:"\u{1f587}",fitzpatrick_scale:!1,category:"objects"},scissors:{keywords:["stationery","cut"],char:"\u2702\ufe0f",fitzpatrick_scale:!1,category:"objects"},triangular_ruler:{keywords:["stationery","math","architect","sketch"],char:"\u{1f4d0}",fitzpatrick_scale:!1,category:"objects"},straight_ruler:{keywords:["stationery","calculate","length","math","school","drawing","architect","sketch"],char:"\u{1f4cf}",fitzpatrick_scale:!1,category:"objects"},abacus:{keywords:["calculation"],char:"\u{1f9ee}",fitzpatrick_scale:!1,category:"objects"},pushpin:{keywords:["stationery","mark","here"],char:"\u{1f4cc}",fitzpatrick_scale:!1,category:"objects"},round_pushpin:{keywords:["stationery","location","map","here"],char:"\u{1f4cd}",fitzpatrick_scale:!1,category:"objects"},triangular_flag_on_post:{keywords:["mark","milestone","place"],char:"\u{1f6a9}",fitzpatrick_scale:!1,category:"objects"},white_flag:{keywords:["losing","loser","lost","surrender","give up","fail"],char:"\u{1f3f3}",fitzpatrick_scale:!1,category:"objects"},black_flag:{keywords:["pirate"],char:"\u{1f3f4}",fitzpatrick_scale:!1,category:"objects"},rainbow_flag:{keywords:["flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"],char:"\u{1f3f3}\ufe0f\u200d\u{1f308}",fitzpatrick_scale:!1,category:"objects"},closed_lock_with_key:{keywords:["security","privacy"],char:"\u{1f510}",fitzpatrick_scale:!1,category:"objects"},lock:{keywords:["security","password","padlock"],char:"\u{1f512}",fitzpatrick_scale:!1,category:"objects"},unlock:{keywords:["privacy","security"],char:"\u{1f513}",fitzpatrick_scale:!1,category:"objects"},lock_with_ink_pen:{keywords:["security","secret"],char:"\u{1f50f}",fitzpatrick_scale:!1,category:"objects"},pen:{keywords:["stationery","writing","write"],char:"\u{1f58a}",fitzpatrick_scale:!1,category:"objects"},fountain_pen:{keywords:["stationery","writing","write"],char:"\u{1f58b}",fitzpatrick_scale:!1,category:"objects"},black_nib:{keywords:["pen","stationery","writing","write"],char:"\u2712\ufe0f",fitzpatrick_scale:!1,category:"objects"},memo:{keywords:["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"],char:"\u{1f4dd}",fitzpatrick_scale:!1,category:"objects"},pencil2:{keywords:["stationery","write","paper","writing","school","study"],char:"\u270f\ufe0f",fitzpatrick_scale:!1,category:"objects"},crayon:{keywords:["drawing","creativity"],char:"\u{1f58d}",fitzpatrick_scale:!1,category:"objects"},paintbrush:{keywords:["drawing","creativity","art"],char:"\u{1f58c}",fitzpatrick_scale:!1,category:"objects"},mag:{keywords:["search","zoom","find","detective"],char:"\u{1f50d}",fitzpatrick_scale:!1,category:"objects"},mag_right:{keywords:["search","zoom","find","detective"],char:"\u{1f50e}",fitzpatrick_scale:!1,category:"objects"},heart:{keywords:["love","like","valentines"],char:"\u2764\ufe0f",fitzpatrick_scale:!1,category:"symbols"},orange_heart:{keywords:["love","like","affection","valentines"],char:"\u{1f9e1}",fitzpatrick_scale:!1,category:"symbols"},yellow_heart:{keywords:["love","like","affection","valentines"],char:"\u{1f49b}",fitzpatrick_scale:!1,category:"symbols"},green_heart:{keywords:["love","like","affection","valentines"],char:"\u{1f49a}",fitzpatrick_scale:!1,category:"symbols"},blue_heart:{keywords:["love","like","affection","valentines"],char:"\u{1f499}",fitzpatrick_scale:!1,category:"symbols"},purple_heart:{keywords:["love","like","affection","valentines"],char:"\u{1f49c}",fitzpatrick_scale:!1,category:"symbols"},black_heart:{keywords:["evil"],char:"\u{1f5a4}",fitzpatrick_scale:!1,category:"symbols"},broken_heart:{keywords:["sad","sorry","break","heart","heartbreak"],char:"\u{1f494}",fitzpatrick_scale:!1,category:"symbols"},heavy_heart_exclamation:{keywords:["decoration","love"],char:"\u2763",fitzpatrick_scale:!1,category:"symbols"},two_hearts:{keywords:["love","like","affection","valentines","heart"],char:"\u{1f495}",fitzpatrick_scale:!1,category:"symbols"},revolving_hearts:{keywords:["love","like","affection","valentines"],char:"\u{1f49e}",fitzpatrick_scale:!1,category:"symbols"},heartbeat:{keywords:["love","like","affection","valentines","pink","heart"],char:"\u{1f493}",fitzpatrick_scale:!1,category:"symbols"},heartpulse:{keywords:["like","love","affection","valentines","pink"],char:"\u{1f497}",fitzpatrick_scale:!1,category:"symbols"},sparkling_heart:{keywords:["love","like","affection","valentines"],char:"\u{1f496}",fitzpatrick_scale:!1,category:"symbols"},cupid:{keywords:["love","like","heart","affection","valentines"],char:"\u{1f498}",fitzpatrick_scale:!1,category:"symbols"},gift_heart:{keywords:["love","valentines"],char:"\u{1f49d}",fitzpatrick_scale:!1,category:"symbols"},heart_decoration:{keywords:["purple-square","love","like"],char:"\u{1f49f}",fitzpatrick_scale:!1,category:"symbols"},peace_symbol:{keywords:["hippie"],char:"\u262e",fitzpatrick_scale:!1,category:"symbols"},latin_cross:{keywords:["christianity"],char:"\u271d",fitzpatrick_scale:!1,category:"symbols"},star_and_crescent:{keywords:["islam"],char:"\u262a",fitzpatrick_scale:!1,category:"symbols"},om:{keywords:["hinduism","buddhism","sikhism","jainism"],char:"\u{1f549}",fitzpatrick_scale:!1,category:"symbols"},wheel_of_dharma:{keywords:["hinduism","buddhism","sikhism","jainism"],char:"\u2638",fitzpatrick_scale:!1,category:"symbols"},star_of_david:{keywords:["judaism"],char:"\u2721",fitzpatrick_scale:!1,category:"symbols"},six_pointed_star:{keywords:["purple-square","religion","jewish","hexagram"],char:"\u{1f52f}",fitzpatrick_scale:!1,category:"symbols"},menorah:{keywords:["hanukkah","candles","jewish"],char:"\u{1f54e}",fitzpatrick_scale:!1,category:"symbols"},yin_yang:{keywords:["balance"],char:"\u262f",fitzpatrick_scale:!1,category:"symbols"},orthodox_cross:{keywords:["suppedaneum","religion"],char:"\u2626",fitzpatrick_scale:!1,category:"symbols"},place_of_worship:{keywords:["religion","church","temple","prayer"],char:"\u{1f6d0}",fitzpatrick_scale:!1,category:"symbols"},ophiuchus:{keywords:["sign","purple-square","constellation","astrology"],char:"\u26ce",fitzpatrick_scale:!1,category:"symbols"},aries:{keywords:["sign","purple-square","zodiac","astrology"],char:"\u2648",fitzpatrick_scale:!1,category:"symbols"},taurus:{keywords:["purple-square","sign","zodiac","astrology"],char:"\u2649",fitzpatrick_scale:!1,category:"symbols"},gemini:{keywords:["sign","zodiac","purple-square","astrology"],char:"\u264a",fitzpatrick_scale:!1,category:"symbols"},cancer:{keywords:["sign","zodiac","purple-square","astrology"],char:"\u264b",fitzpatrick_scale:!1,category:"symbols"},leo:{keywords:["sign","purple-square","zodiac","astrology"],char:"\u264c",fitzpatrick_scale:!1,category:"symbols"},virgo:{keywords:["sign","zodiac","purple-square","astrology"],char:"\u264d",fitzpatrick_scale:!1,category:"symbols"},libra:{keywords:["sign","purple-square","zodiac","astrology"],char:"\u264e",fitzpatrick_scale:!1,category:"symbols"},scorpius:{keywords:["sign","zodiac","purple-square","astrology","scorpio"],char:"\u264f",fitzpatrick_scale:!1,category:"symbols"},sagittarius:{keywords:["sign","zodiac","purple-square","astrology"],char:"\u2650",fitzpatrick_scale:!1,category:"symbols"},capricorn:{keywords:["sign","zodiac","purple-square","astrology"],char:"\u2651",fitzpatrick_scale:!1,category:"symbols"},aquarius:{keywords:["sign","purple-square","zodiac","astrology"],char:"\u2652",fitzpatrick_scale:!1,category:"symbols"},pisces:{keywords:["purple-square","sign","zodiac","astrology"],char:"\u2653",fitzpatrick_scale:!1,category:"symbols"},id:{keywords:["purple-square","words"],char:"\u{1f194}",fitzpatrick_scale:!1,category:"symbols"},atom_symbol:{keywords:["science","physics","chemistry"],char:"\u269b",fitzpatrick_scale:!1,category:"symbols"},u7a7a:{keywords:["kanji","japanese","chinese","empty","sky","blue-square"],char:"\u{1f233}",fitzpatrick_scale:!1,category:"symbols"},u5272:{keywords:["cut","divide","chinese","kanji","pink-square"],char:"\u{1f239}",fitzpatrick_scale:!1,category:"symbols"},radioactive:{keywords:["nuclear","danger"],char:"\u2622",fitzpatrick_scale:!1,category:"symbols"},biohazard:{keywords:["danger"],char:"\u2623",fitzpatrick_scale:!1,category:"symbols"},mobile_phone_off:{keywords:["mute","orange-square","silence","quiet"],char:"\u{1f4f4}",fitzpatrick_scale:!1,category:"symbols"},vibration_mode:{keywords:["orange-square","phone"],char:"\u{1f4f3}",fitzpatrick_scale:!1,category:"symbols"},u6709:{keywords:["orange-square","chinese","have","kanji"],char:"\u{1f236}",fitzpatrick_scale:!1,category:"symbols"},u7121:{keywords:["nothing","chinese","kanji","japanese","orange-square"],char:"\u{1f21a}",fitzpatrick_scale:!1,category:"symbols"},u7533:{keywords:["chinese","japanese","kanji","orange-square"],char:"\u{1f238}",fitzpatrick_scale:!1,category:"symbols"},u55b6:{keywords:["japanese","opening hours","orange-square"],char:"\u{1f23a}",fitzpatrick_scale:!1,category:"symbols"},u6708:{keywords:["chinese","month","moon","japanese","orange-square","kanji"],char:"\u{1f237}\ufe0f",fitzpatrick_scale:!1,category:"symbols"},eight_pointed_black_star:{keywords:["orange-square","shape","polygon"],char:"\u2734\ufe0f",fitzpatrick_scale:!1,category:"symbols"},vs:{keywords:["words","orange-square"],char:"\u{1f19a}",fitzpatrick_scale:!1,category:"symbols"},accept:{keywords:["ok","good","chinese","kanji","agree","yes","orange-circle"],char:"\u{1f251}",fitzpatrick_scale:!1,category:"symbols"},white_flower:{keywords:["japanese","spring"],char:"\u{1f4ae}",fitzpatrick_scale:!1,category:"symbols"},ideograph_advantage:{keywords:["chinese","kanji","obtain","get","circle"],char:"\u{1f250}",fitzpatrick_scale:!1,category:"symbols"},secret:{keywords:["privacy","chinese","sshh","kanji","red-circle"],char:"\u3299\ufe0f",fitzpatrick_scale:!1,category:"symbols"},congratulations:{keywords:["chinese","kanji","japanese","red-circle"],char:"\u3297\ufe0f",fitzpatrick_scale:!1,category:"symbols"},u5408:{keywords:["japanese","chinese","join","kanji","red-square"],char:"\u{1f234}",fitzpatrick_scale:!1,category:"symbols"},u6e80:{keywords:["full","chinese","japanese","red-square","kanji"],char:"\u{1f235}",fitzpatrick_scale:!1,category:"symbols"},u7981:{keywords:["kanji","japanese","chinese","forbidden","limit","restricted","red-square"],char:"\u{1f232}",fitzpatrick_scale:!1,category:"symbols"},a:{keywords:["red-square","alphabet","letter"],char:"\u{1f170}\ufe0f",fitzpatrick_scale:!1,category:"symbols"},b:{keywords:["red-square","alphabet","letter"],char:"\u{1f171}\ufe0f",fitzpatrick_scale:!1,category:"symbols"},ab:{keywords:["red-square","alphabet"],char:"\u{1f18e}",fitzpatrick_scale:!1,category:"symbols"},cl:{keywords:["alphabet","words","red-square"],char:"\u{1f191}",fitzpatrick_scale:!1,category:"symbols"},o2:{keywords:["alphabet","red-square","letter"],char:"\u{1f17e}\ufe0f",fitzpatrick_scale:!1,category:"symbols"},sos:{keywords:["help","red-square","words","emergency","911"],char:"\u{1f198}",fitzpatrick_scale:!1,category:"symbols"},no_entry:{keywords:["limit","security","privacy","bad","denied","stop","circle"],char:"\u26d4",fitzpatrick_scale:!1,category:"symbols"},name_badge:{keywords:["fire","forbid"],char:"\u{1f4db}",fitzpatrick_scale:!1,category:"symbols"},no_entry_sign:{keywords:["forbid","stop","limit","denied","disallow","circle"],char:"\u{1f6ab}",fitzpatrick_scale:!1,category:"symbols"},x:{keywords:["no","delete","remove","cancel","red"],char:"\u274c",fitzpatrick_scale:!1,category:"symbols"},o:{keywords:["circle","round"],char:"\u2b55",fitzpatrick_scale:!1,category:"symbols"},stop_sign:{keywords:["stop"],char:"\u{1f6d1}",fitzpatrick_scale:!1,category:"symbols"},anger:{keywords:["angry","mad"],char:"\u{1f4a2}",fitzpatrick_scale:!1,category:"symbols"},hotsprings:{keywords:["bath","warm","relax"],char:"\u2668\ufe0f",fitzpatrick_scale:!1,category:"symbols"},no_pedestrians:{keywords:["rules","crossing","walking","circle"],char:"\u{1f6b7}",fitzpatrick_scale:!1,category:"symbols"},do_not_litter:{keywords:["trash","bin","garbage","circle"],char:"\u{1f6af}",fitzpatrick_scale:!1,category:"symbols"},no_bicycles:{keywords:["cyclist","prohibited","circle"],char:"\u{1f6b3}",fitzpatrick_scale:!1,category:"symbols"},"non-potable_water":{keywords:["drink","faucet","tap","circle"],char:"\u{1f6b1}",fitzpatrick_scale:!1,category:"symbols"},underage:{keywords:["18","drink","pub","night","minor","circle"],char:"\u{1f51e}",fitzpatrick_scale:!1,category:"symbols"},no_mobile_phones:{keywords:["iphone","mute","circle"],char:"\u{1f4f5}",fitzpatrick_scale:!1,category:"symbols"},exclamation:{keywords:["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"],char:"\u2757",fitzpatrick_scale:!1,category:"symbols"},grey_exclamation:{keywords:["surprise","punctuation","gray","wow","warning"],char:"\u2755",fitzpatrick_scale:!1,category:"symbols"},question:{keywords:["doubt","confused"],char:"\u2753",fitzpatrick_scale:!1,category:"symbols"},grey_question:{keywords:["doubts","gray","huh","confused"],char:"\u2754",fitzpatrick_scale:!1,category:"symbols"},bangbang:{keywords:["exclamation","surprise"],char:"\u203c\ufe0f",fitzpatrick_scale:!1,category:"symbols"},interrobang:{keywords:["wat","punctuation","surprise"],char:"\u2049\ufe0f",fitzpatrick_scale:!1,category:"symbols"},100:{keywords:["score","perfect","numbers","century","exam","quiz","test","pass","hundred"],char:"\u{1f4af}",fitzpatrick_scale:!1,category:"symbols"},low_brightness:{keywords:["sun","afternoon","warm","summer"],char:"\u{1f505}",fitzpatrick_scale:!1,category:"symbols"},high_brightness:{keywords:["sun","light"],char:"\u{1f506}",fitzpatrick_scale:!1,category:"symbols"},trident:{keywords:["weapon","spear"],char:"\u{1f531}",fitzpatrick_scale:!1,category:"symbols"},fleur_de_lis:{keywords:["decorative","scout"],char:"\u269c",fitzpatrick_scale:!1,category:"symbols"},part_alternation_mark:{keywords:["graph","presentation","stats","business","economics","bad"],char:"\u303d\ufe0f",fitzpatrick_scale:!1,category:"symbols"},warning:{keywords:["exclamation","wip","alert","error","problem","issue"],char:"\u26a0\ufe0f",fitzpatrick_scale:!1,category:"symbols"},children_crossing:{keywords:["school","warning","danger","sign","driving","yellow-diamond"],char:"\u{1f6b8}",fitzpatrick_scale:!1,category:"symbols"},beginner:{keywords:["badge","shield"],char:"\u{1f530}",fitzpatrick_scale:!1,category:"symbols"},recycle:{keywords:["arrow","environment","garbage","trash"],char:"\u267b\ufe0f",fitzpatrick_scale:!1,category:"symbols"},u6307:{keywords:["chinese","point","green-square","kanji"],char:"\u{1f22f}",fitzpatrick_scale:!1,category:"symbols"},chart:{keywords:["green-square","graph","presentation","stats"],char:"\u{1f4b9}",fitzpatrick_scale:!1,category:"symbols"},sparkle:{keywords:["stars","green-square","awesome","good","fireworks"],char:"\u2747\ufe0f",fitzpatrick_scale:!1,category:"symbols"},eight_spoked_asterisk:{keywords:["star","sparkle","green-square"],char:"\u2733\ufe0f",fitzpatrick_scale:!1,category:"symbols"},negative_squared_cross_mark:{keywords:["x","green-square","no","deny"],char:"\u274e",fitzpatrick_scale:!1,category:"symbols"},white_check_mark:{keywords:["green-square","ok","agree","vote","election","answer","tick"],char:"\u2705",fitzpatrick_scale:!1,category:"symbols"},diamond_shape_with_a_dot_inside:{keywords:["jewel","blue","gem","crystal","fancy"],char:"\u{1f4a0}",fitzpatrick_scale:!1,category:"symbols"},cyclone:{keywords:["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],char:"\u{1f300}",fitzpatrick_scale:!1,category:"symbols"},loop:{keywords:["tape","cassette"],char:"\u27bf",fitzpatrick_scale:!1,category:"symbols"},globe_with_meridians:{keywords:["earth","international","world","internet","interweb","i18n"],char:"\u{1f310}",fitzpatrick_scale:!1,category:"symbols"},m:{keywords:["alphabet","blue-circle","letter"],char:"\u24c2\ufe0f",fitzpatrick_scale:!1,category:"symbols"},atm:{keywords:["money","sales","cash","blue-square","payment","bank"],char:"\u{1f3e7}",fitzpatrick_scale:!1,category:"symbols"},sa:{keywords:["japanese","blue-square","katakana"],char:"\u{1f202}\ufe0f",fitzpatrick_scale:!1,category:"symbols"},passport_control:{keywords:["custom","blue-square"],char:"\u{1f6c2}",fitzpatrick_scale:!1,category:"symbols"},customs:{keywords:["passport","border","blue-square"],char:"\u{1f6c3}",fitzpatrick_scale:!1,category:"symbols"},baggage_claim:{keywords:["blue-square","airport","transport"],char:"\u{1f6c4}",fitzpatrick_scale:!1,category:"symbols"},left_luggage:{keywords:["blue-square","travel"],char:"\u{1f6c5}",fitzpatrick_scale:!1,category:"symbols"},wheelchair:{keywords:["blue-square","disabled","a11y","accessibility"],char:"\u267f",fitzpatrick_scale:!1,category:"symbols"},no_smoking:{keywords:["cigarette","blue-square","smell","smoke"],char:"\u{1f6ad}",fitzpatrick_scale:!1,category:"symbols"},wc:{keywords:["toilet","restroom","blue-square"],char:"\u{1f6be}",fitzpatrick_scale:!1,category:"symbols"},parking:{keywords:["cars","blue-square","alphabet","letter"],char:"\u{1f17f}\ufe0f",fitzpatrick_scale:!1,category:"symbols"},potable_water:{keywords:["blue-square","liquid","restroom","cleaning","faucet"],char:"\u{1f6b0}",fitzpatrick_scale:!1,category:"symbols"},mens:{keywords:["toilet","restroom","wc","blue-square","gender","male"],char:"\u{1f6b9}",fitzpatrick_scale:!1,category:"symbols"},womens:{keywords:["purple-square","woman","female","toilet","loo","restroom","gender"],char:"\u{1f6ba}",fitzpatrick_scale:!1,category:"symbols"},baby_symbol:{keywords:["orange-square","child"],char:"\u{1f6bc}",fitzpatrick_scale:!1,category:"symbols"},restroom:{keywords:["blue-square","toilet","refresh","wc","gender"],char:"\u{1f6bb}",fitzpatrick_scale:!1,category:"symbols"},put_litter_in_its_place:{keywords:["blue-square","sign","human","info"],char:"\u{1f6ae}",fitzpatrick_scale:!1,category:"symbols"},cinema:{keywords:["blue-square","record","film","movie","curtain","stage","theater"],char:"\u{1f3a6}",fitzpatrick_scale:!1,category:"symbols"},signal_strength:{keywords:["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"],char:"\u{1f4f6}",fitzpatrick_scale:!1,category:"symbols"},koko:{keywords:["blue-square","here","katakana","japanese","destination"],char:"\u{1f201}",fitzpatrick_scale:!1,category:"symbols"},ng:{keywords:["blue-square","words","shape","icon"],char:"\u{1f196}",fitzpatrick_scale:!1,category:"symbols"},ok:{keywords:["good","agree","yes","blue-square"],char:"\u{1f197}",fitzpatrick_scale:!1,category:"symbols"},up:{keywords:["blue-square","above","high"],char:"\u{1f199}",fitzpatrick_scale:!1,category:"symbols"},cool:{keywords:["words","blue-square"],char:"\u{1f192}",fitzpatrick_scale:!1,category:"symbols"},new:{keywords:["blue-square","words","start"],char:"\u{1f195}",fitzpatrick_scale:!1,category:"symbols"},free:{keywords:["blue-square","words"],char:"\u{1f193}",fitzpatrick_scale:!1,category:"symbols"},zero:{keywords:["0","numbers","blue-square","null"],char:"0\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},one:{keywords:["blue-square","numbers","1"],char:"1\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},two:{keywords:["numbers","2","prime","blue-square"],char:"2\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},three:{keywords:["3","numbers","prime","blue-square"],char:"3\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},four:{keywords:["4","numbers","blue-square"],char:"4\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},five:{keywords:["5","numbers","blue-square","prime"],char:"5\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},six:{keywords:["6","numbers","blue-square"],char:"6\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},seven:{keywords:["7","numbers","blue-square","prime"],char:"7\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},eight:{keywords:["8","blue-square","numbers"],char:"8\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},nine:{keywords:["blue-square","numbers","9"],char:"9\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},keycap_ten:{keywords:["numbers","10","blue-square"],char:"\u{1f51f}",fitzpatrick_scale:!1,category:"symbols"},asterisk:{keywords:["star","keycap"],char:"*\u20e3",fitzpatrick_scale:!1,category:"symbols"},1234:{keywords:["numbers","blue-square"],char:"\u{1f522}",fitzpatrick_scale:!1,category:"symbols"},eject_button:{keywords:["blue-square"],char:"\u23cf\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_forward:{keywords:["blue-square","right","direction","play"],char:"\u25b6\ufe0f",fitzpatrick_scale:!1,category:"symbols"},pause_button:{keywords:["pause","blue-square"],char:"\u23f8",fitzpatrick_scale:!1,category:"symbols"},next_track_button:{keywords:["forward","next","blue-square"],char:"\u23ed",fitzpatrick_scale:!1,category:"symbols"},stop_button:{keywords:["blue-square"],char:"\u23f9",fitzpatrick_scale:!1,category:"symbols"},record_button:{keywords:["blue-square"],char:"\u23fa",fitzpatrick_scale:!1,category:"symbols"},play_or_pause_button:{keywords:["blue-square","play","pause"],char:"\u23ef",fitzpatrick_scale:!1,category:"symbols"},previous_track_button:{keywords:["backward"],char:"\u23ee",fitzpatrick_scale:!1,category:"symbols"},fast_forward:{keywords:["blue-square","play","speed","continue"],char:"\u23e9",fitzpatrick_scale:!1,category:"symbols"},rewind:{keywords:["play","blue-square"],char:"\u23ea",fitzpatrick_scale:!1,category:"symbols"},twisted_rightwards_arrows:{keywords:["blue-square","shuffle","music","random"],char:"\u{1f500}",fitzpatrick_scale:!1,category:"symbols"},repeat:{keywords:["loop","record"],char:"\u{1f501}",fitzpatrick_scale:!1,category:"symbols"},repeat_one:{keywords:["blue-square","loop"],char:"\u{1f502}",fitzpatrick_scale:!1,category:"symbols"},arrow_backward:{keywords:["blue-square","left","direction"],char:"\u25c0\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_up_small:{keywords:["blue-square","triangle","direction","point","forward","top"],char:"\u{1f53c}",fitzpatrick_scale:!1,category:"symbols"},arrow_down_small:{keywords:["blue-square","direction","bottom"],char:"\u{1f53d}",fitzpatrick_scale:!1,category:"symbols"},arrow_double_up:{keywords:["blue-square","direction","top"],char:"\u23eb",fitzpatrick_scale:!1,category:"symbols"},arrow_double_down:{keywords:["blue-square","direction","bottom"],char:"\u23ec",fitzpatrick_scale:!1,category:"symbols"},arrow_right:{keywords:["blue-square","next"],char:"\u27a1\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_left:{keywords:["blue-square","previous","back"],char:"\u2b05\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_up:{keywords:["blue-square","continue","top","direction"],char:"\u2b06\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_down:{keywords:["blue-square","direction","bottom"],char:"\u2b07\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_upper_right:{keywords:["blue-square","point","direction","diagonal","northeast"],char:"\u2197\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_lower_right:{keywords:["blue-square","direction","diagonal","southeast"],char:"\u2198\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_lower_left:{keywords:["blue-square","direction","diagonal","southwest"],char:"\u2199\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_upper_left:{keywords:["blue-square","point","direction","diagonal","northwest"],char:"\u2196\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_up_down:{keywords:["blue-square","direction","way","vertical"],char:"\u2195\ufe0f",fitzpatrick_scale:!1,category:"symbols"},left_right_arrow:{keywords:["shape","direction","horizontal","sideways"],char:"\u2194\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrows_counterclockwise:{keywords:["blue-square","sync","cycle"],char:"\u{1f504}",fitzpatrick_scale:!1,category:"symbols"},arrow_right_hook:{keywords:["blue-square","return","rotate","direction"],char:"\u21aa\ufe0f",fitzpatrick_scale:!1,category:"symbols"},leftwards_arrow_with_hook:{keywords:["back","return","blue-square","undo","enter"],char:"\u21a9\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_heading_up:{keywords:["blue-square","direction","top"],char:"\u2934\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrow_heading_down:{keywords:["blue-square","direction","bottom"],char:"\u2935\ufe0f",fitzpatrick_scale:!1,category:"symbols"},hash:{keywords:["symbol","blue-square","twitter"],char:"#\ufe0f\u20e3",fitzpatrick_scale:!1,category:"symbols"},information_source:{keywords:["blue-square","alphabet","letter"],char:"\u2139\ufe0f",fitzpatrick_scale:!1,category:"symbols"},abc:{keywords:["blue-square","alphabet"],char:"\u{1f524}",fitzpatrick_scale:!1,category:"symbols"},abcd:{keywords:["blue-square","alphabet"],char:"\u{1f521}",fitzpatrick_scale:!1,category:"symbols"},capital_abcd:{keywords:["alphabet","words","blue-square"],char:"\u{1f520}",fitzpatrick_scale:!1,category:"symbols"},symbols:{keywords:["blue-square","music","note","ampersand","percent","glyphs","characters"],char:"\u{1f523}",fitzpatrick_scale:!1,category:"symbols"},musical_note:{keywords:["score","tone","sound"],char:"\u{1f3b5}",fitzpatrick_scale:!1,category:"symbols"},notes:{keywords:["music","score"],char:"\u{1f3b6}",fitzpatrick_scale:!1,category:"symbols"},wavy_dash:{keywords:["draw","line","moustache","mustache","squiggle","scribble"],char:"\u3030\ufe0f",fitzpatrick_scale:!1,category:"symbols"},curly_loop:{keywords:["scribble","draw","shape","squiggle"],char:"\u27b0",fitzpatrick_scale:!1,category:"symbols"},heavy_check_mark:{keywords:["ok","nike","answer","yes","tick"],char:"\u2714\ufe0f",fitzpatrick_scale:!1,category:"symbols"},arrows_clockwise:{keywords:["sync","cycle","round","repeat"],char:"\u{1f503}",fitzpatrick_scale:!1,category:"symbols"},heavy_plus_sign:{keywords:["math","calculation","addition","more","increase"],char:"\u2795",fitzpatrick_scale:!1,category:"symbols"},heavy_minus_sign:{keywords:["math","calculation","subtract","less"],char:"\u2796",fitzpatrick_scale:!1,category:"symbols"},heavy_division_sign:{keywords:["divide","math","calculation"],char:"\u2797",fitzpatrick_scale:!1,category:"symbols"},heavy_multiplication_x:{keywords:["math","calculation"],char:"\u2716\ufe0f",fitzpatrick_scale:!1,category:"symbols"},infinity:{keywords:["forever"],char:"\u267e",fitzpatrick_scale:!1,category:"symbols"},heavy_dollar_sign:{keywords:["money","sales","payment","currency","buck"],char:"\u{1f4b2}",fitzpatrick_scale:!1,category:"symbols"},currency_exchange:{keywords:["money","sales","dollar","travel"],char:"\u{1f4b1}",fitzpatrick_scale:!1,category:"symbols"},copyright:{keywords:["ip","license","circle","law","legal"],char:"\xa9\ufe0f",fitzpatrick_scale:!1,category:"symbols"},registered:{keywords:["alphabet","circle"],char:"\xae\ufe0f",fitzpatrick_scale:!1,category:"symbols"},tm:{keywords:["trademark","brand","law","legal"],char:"\u2122\ufe0f",fitzpatrick_scale:!1,category:"symbols"},end:{keywords:["words","arrow"],char:"\u{1f51a}",fitzpatrick_scale:!1,category:"symbols"},back:{keywords:["arrow","words","return"],char:"\u{1f519}",fitzpatrick_scale:!1,category:"symbols"},on:{keywords:["arrow","words"],char:"\u{1f51b}",fitzpatrick_scale:!1,category:"symbols"},top:{keywords:["words","blue-square"],char:"\u{1f51d}",fitzpatrick_scale:!1,category:"symbols"},soon:{keywords:["arrow","words"],char:"\u{1f51c}",fitzpatrick_scale:!1,category:"symbols"},ballot_box_with_check:{keywords:["ok","agree","confirm","black-square","vote","election","yes","tick"],char:"\u2611\ufe0f",fitzpatrick_scale:!1,category:"symbols"},radio_button:{keywords:["input","old","music","circle"],char:"\u{1f518}",fitzpatrick_scale:!1,category:"symbols"},white_circle:{keywords:["shape","round"],char:"\u26aa",fitzpatrick_scale:!1,category:"symbols"},black_circle:{keywords:["shape","button","round"],char:"\u26ab",fitzpatrick_scale:!1,category:"symbols"},red_circle:{keywords:["shape","error","danger"],char:"\u{1f534}",fitzpatrick_scale:!1,category:"symbols"},large_blue_circle:{keywords:["shape","icon","button"],char:"\u{1f535}",fitzpatrick_scale:!1,category:"symbols"},small_orange_diamond:{keywords:["shape","jewel","gem"],char:"\u{1f538}",fitzpatrick_scale:!1,category:"symbols"},small_blue_diamond:{keywords:["shape","jewel","gem"],char:"\u{1f539}",fitzpatrick_scale:!1,category:"symbols"},large_orange_diamond:{keywords:["shape","jewel","gem"],char:"\u{1f536}",fitzpatrick_scale:!1,category:"symbols"},large_blue_diamond:{keywords:["shape","jewel","gem"],char:"\u{1f537}",fitzpatrick_scale:!1,category:"symbols"},small_red_triangle:{keywords:["shape","direction","up","top"],char:"\u{1f53a}",fitzpatrick_scale:!1,category:"symbols"},black_small_square:{keywords:["shape","icon"],char:"\u25aa\ufe0f",fitzpatrick_scale:!1,category:"symbols"},white_small_square:{keywords:["shape","icon"],char:"\u25ab\ufe0f",fitzpatrick_scale:!1,category:"symbols"},black_large_square:{keywords:["shape","icon","button"],char:"\u2b1b",fitzpatrick_scale:!1,category:"symbols"},white_large_square:{keywords:["shape","icon","stone","button"],char:"\u2b1c",fitzpatrick_scale:!1,category:"symbols"},small_red_triangle_down:{keywords:["shape","direction","bottom"],char:"\u{1f53b}",fitzpatrick_scale:!1,category:"symbols"},black_medium_square:{keywords:["shape","button","icon"],char:"\u25fc\ufe0f",fitzpatrick_scale:!1,category:"symbols"},white_medium_square:{keywords:["shape","stone","icon"],char:"\u25fb\ufe0f",fitzpatrick_scale:!1,category:"symbols"},black_medium_small_square:{keywords:["icon","shape","button"],char:"\u25fe",fitzpatrick_scale:!1,category:"symbols"},white_medium_small_square:{keywords:["shape","stone","icon","button"],char:"\u25fd",fitzpatrick_scale:!1,category:"symbols"},black_square_button:{keywords:["shape","input","frame"],char:"\u{1f532}",fitzpatrick_scale:!1,category:"symbols"},white_square_button:{keywords:["shape","input"],char:"\u{1f533}",fitzpatrick_scale:!1,category:"symbols"},speaker:{keywords:["sound","volume","silence","broadcast"],char:"\u{1f508}",fitzpatrick_scale:!1,category:"symbols"},sound:{keywords:["volume","speaker","broadcast"],char:"\u{1f509}",fitzpatrick_scale:!1,category:"symbols"},loud_sound:{keywords:["volume","noise","noisy","speaker","broadcast"],char:"\u{1f50a}",fitzpatrick_scale:!1,category:"symbols"},mute:{keywords:["sound","volume","silence","quiet"],char:"\u{1f507}",fitzpatrick_scale:!1,category:"symbols"},mega:{keywords:["sound","speaker","volume"],char:"\u{1f4e3}",fitzpatrick_scale:!1,category:"symbols"},loudspeaker:{keywords:["volume","sound"],char:"\u{1f4e2}",fitzpatrick_scale:!1,category:"symbols"},bell:{keywords:["sound","notification","christmas","xmas","chime"],char:"\u{1f514}",fitzpatrick_scale:!1,category:"symbols"},no_bell:{keywords:["sound","volume","mute","quiet","silent"],char:"\u{1f515}",fitzpatrick_scale:!1,category:"symbols"},black_joker:{keywords:["poker","cards","game","play","magic"],char:"\u{1f0cf}",fitzpatrick_scale:!1,category:"symbols"},mahjong:{keywords:["game","play","chinese","kanji"],char:"\u{1f004}",fitzpatrick_scale:!1,category:"symbols"},spades:{keywords:["poker","cards","suits","magic"],char:"\u2660\ufe0f",fitzpatrick_scale:!1,category:"symbols"},clubs:{keywords:["poker","cards","magic","suits"],char:"\u2663\ufe0f",fitzpatrick_scale:!1,category:"symbols"},hearts:{keywords:["poker","cards","magic","suits"],char:"\u2665\ufe0f",fitzpatrick_scale:!1,category:"symbols"},diamonds:{keywords:["poker","cards","magic","suits"],char:"\u2666\ufe0f",fitzpatrick_scale:!1,category:"symbols"},flower_playing_cards:{keywords:["game","sunset","red"],char:"\u{1f3b4}",fitzpatrick_scale:!1,category:"symbols"},thought_balloon:{keywords:["bubble","cloud","speech","thinking","dream"],char:"\u{1f4ad}",fitzpatrick_scale:!1,category:"symbols"},right_anger_bubble:{keywords:["caption","speech","thinking","mad"],char:"\u{1f5ef}",fitzpatrick_scale:!1,category:"symbols"},speech_balloon:{keywords:["bubble","words","message","talk","chatting"],char:"\u{1f4ac}",fitzpatrick_scale:!1,category:"symbols"},left_speech_bubble:{keywords:["words","message","talk","chatting"],char:"\u{1f5e8}",fitzpatrick_scale:!1,category:"symbols"},clock1:{keywords:["time","late","early","schedule"],char:"\u{1f550}",fitzpatrick_scale:!1,category:"symbols"},clock2:{keywords:["time","late","early","schedule"],char:"\u{1f551}",fitzpatrick_scale:!1,category:"symbols"},clock3:{keywords:["time","late","early","schedule"],char:"\u{1f552}",fitzpatrick_scale:!1,category:"symbols"},clock4:{keywords:["time","late","early","schedule"],char:"\u{1f553}",fitzpatrick_scale:!1,category:"symbols"},clock5:{keywords:["time","late","early","schedule"],char:"\u{1f554}",fitzpatrick_scale:!1,category:"symbols"},clock6:{keywords:["time","late","early","schedule","dawn","dusk"],char:"\u{1f555}",fitzpatrick_scale:!1,category:"symbols"},clock7:{keywords:["time","late","early","schedule"],char:"\u{1f556}",fitzpatrick_scale:!1,category:"symbols"},clock8:{keywords:["time","late","early","schedule"],char:"\u{1f557}",fitzpatrick_scale:!1,category:"symbols"},clock9:{keywords:["time","late","early","schedule"],char:"\u{1f558}",fitzpatrick_scale:!1,category:"symbols"},clock10:{keywords:["time","late","early","schedule"],char:"\u{1f559}",fitzpatrick_scale:!1,category:"symbols"},clock11:{keywords:["time","late","early","schedule"],char:"\u{1f55a}",fitzpatrick_scale:!1,category:"symbols"},clock12:{keywords:["time","noon","midnight","midday","late","early","schedule"],char:"\u{1f55b}",fitzpatrick_scale:!1,category:"symbols"},clock130:{keywords:["time","late","early","schedule"],char:"\u{1f55c}",fitzpatrick_scale:!1,category:"symbols"},clock230:{keywords:["time","late","early","schedule"],char:"\u{1f55d}",fitzpatrick_scale:!1,category:"symbols"},clock330:{keywords:["time","late","early","schedule"],char:"\u{1f55e}",fitzpatrick_scale:!1,category:"symbols"},clock430:{keywords:["time","late","early","schedule"],char:"\u{1f55f}",fitzpatrick_scale:!1,category:"symbols"},clock530:{keywords:["time","late","early","schedule"],char:"\u{1f560}",fitzpatrick_scale:!1,category:"symbols"},clock630:{keywords:["time","late","early","schedule"],char:"\u{1f561}",fitzpatrick_scale:!1,category:"symbols"},clock730:{keywords:["time","late","early","schedule"],char:"\u{1f562}",fitzpatrick_scale:!1,category:"symbols"},clock830:{keywords:["time","late","early","schedule"],char:"\u{1f563}",fitzpatrick_scale:!1,category:"symbols"},clock930:{keywords:["time","late","early","schedule"],char:"\u{1f564}",fitzpatrick_scale:!1,category:"symbols"},clock1030:{keywords:["time","late","early","schedule"],char:"\u{1f565}",fitzpatrick_scale:!1,category:"symbols"},clock1130:{keywords:["time","late","early","schedule"],char:"\u{1f566}",fitzpatrick_scale:!1,category:"symbols"},clock1230:{keywords:["time","late","early","schedule"],char:"\u{1f567}",fitzpatrick_scale:!1,category:"symbols"},afghanistan:{keywords:["af","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},aland_islands:{keywords:["\xc5land","islands","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1fd}",fitzpatrick_scale:!1,category:"flags"},albania:{keywords:["al","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},algeria:{keywords:["dz","flag","nation","country","banner"],char:"\u{1f1e9}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},american_samoa:{keywords:["american","ws","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},andorra:{keywords:["ad","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},angola:{keywords:["ao","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},anguilla:{keywords:["ai","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},antarctica:{keywords:["aq","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1f6}",fitzpatrick_scale:!1,category:"flags"},antigua_barbuda:{keywords:["antigua","barbuda","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},argentina:{keywords:["ar","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},armenia:{keywords:["am","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},aruba:{keywords:["aw","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},australia:{keywords:["au","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},austria:{keywords:["at","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},azerbaijan:{keywords:["az","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},bahamas:{keywords:["bs","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},bahrain:{keywords:["bh","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},bangladesh:{keywords:["bd","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},barbados:{keywords:["bb","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1e7}",fitzpatrick_scale:!1,category:"flags"},belarus:{keywords:["by","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},belgium:{keywords:["be","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},belize:{keywords:["bz","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},benin:{keywords:["bj","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1ef}",fitzpatrick_scale:!1,category:"flags"},bermuda:{keywords:["bm","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},bhutan:{keywords:["bt","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},bolivia:{keywords:["bo","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},caribbean_netherlands:{keywords:["bonaire","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f6}",fitzpatrick_scale:!1,category:"flags"},bosnia_herzegovina:{keywords:["bosnia","herzegovina","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},botswana:{keywords:["bw","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},brazil:{keywords:["br","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},british_indian_ocean_territory:{keywords:["british","indian","ocean","territory","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},british_virgin_islands:{keywords:["british","virgin","islands","bvi","flag","nation","country","banner"],char:"\u{1f1fb}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},brunei:{keywords:["bn","darussalam","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},bulgaria:{keywords:["bg","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},burkina_faso:{keywords:["burkina","faso","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},burundi:{keywords:["bi","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},cape_verde:{keywords:["cabo","verde","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1fb}",fitzpatrick_scale:!1,category:"flags"},cambodia:{keywords:["kh","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},cameroon:{keywords:["cm","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},canada:{keywords:["ca","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},canary_islands:{keywords:["canary","islands","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},cayman_islands:{keywords:["cayman","islands","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},central_african_republic:{keywords:["central","african","republic","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},chad:{keywords:["td","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},chile:{keywords:["flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},cn:{keywords:["china","chinese","prc","flag","country","nation","banner"],char:"\u{1f1e8}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},christmas_island:{keywords:["christmas","island","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1fd}",fitzpatrick_scale:!1,category:"flags"},cocos_islands:{keywords:["cocos","keeling","islands","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},colombia:{keywords:["co","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},comoros:{keywords:["km","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},congo_brazzaville:{keywords:["congo","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},congo_kinshasa:{keywords:["congo","democratic","republic","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},cook_islands:{keywords:["cook","islands","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},costa_rica:{keywords:["costa","rica","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},croatia:{keywords:["hr","flag","nation","country","banner"],char:"\u{1f1ed}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},cuba:{keywords:["cu","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},curacao:{keywords:["cura\xe7ao","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},cyprus:{keywords:["cy","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},czech_republic:{keywords:["cz","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},denmark:{keywords:["dk","flag","nation","country","banner"],char:"\u{1f1e9}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},djibouti:{keywords:["dj","flag","nation","country","banner"],char:"\u{1f1e9}\u{1f1ef}",fitzpatrick_scale:!1,category:"flags"},dominica:{keywords:["dm","flag","nation","country","banner"],char:"\u{1f1e9}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},dominican_republic:{keywords:["dominican","republic","flag","nation","country","banner"],char:"\u{1f1e9}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},ecuador:{keywords:["ec","flag","nation","country","banner"],char:"\u{1f1ea}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},egypt:{keywords:["eg","flag","nation","country","banner"],char:"\u{1f1ea}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},el_salvador:{keywords:["el","salvador","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1fb}",fitzpatrick_scale:!1,category:"flags"},equatorial_guinea:{keywords:["equatorial","gn","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f6}",fitzpatrick_scale:!1,category:"flags"},eritrea:{keywords:["er","flag","nation","country","banner"],char:"\u{1f1ea}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},estonia:{keywords:["ee","flag","nation","country","banner"],char:"\u{1f1ea}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},ethiopia:{keywords:["et","flag","nation","country","banner"],char:"\u{1f1ea}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},eu:{keywords:["european","union","flag","banner"],char:"\u{1f1ea}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},falkland_islands:{keywords:["falkland","islands","malvinas","flag","nation","country","banner"],char:"\u{1f1eb}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},faroe_islands:{keywords:["faroe","islands","flag","nation","country","banner"],char:"\u{1f1eb}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},fiji:{keywords:["fj","flag","nation","country","banner"],char:"\u{1f1eb}\u{1f1ef}",fitzpatrick_scale:!1,category:"flags"},finland:{keywords:["fi","flag","nation","country","banner"],char:"\u{1f1eb}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},fr:{keywords:["banner","flag","nation","france","french","country"],char:"\u{1f1eb}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},french_guiana:{keywords:["french","guiana","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},french_polynesia:{keywords:["french","polynesia","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},french_southern_territories:{keywords:["french","southern","territories","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},gabon:{keywords:["ga","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},gambia:{keywords:["gm","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},georgia:{keywords:["ge","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},de:{keywords:["german","nation","flag","country","banner"],char:"\u{1f1e9}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},ghana:{keywords:["gh","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},gibraltar:{keywords:["gi","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},greece:{keywords:["gr","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},greenland:{keywords:["gl","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},grenada:{keywords:["gd","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},guadeloupe:{keywords:["gp","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f5}",fitzpatrick_scale:!1,category:"flags"},guam:{keywords:["gu","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},guatemala:{keywords:["gt","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},guernsey:{keywords:["gg","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},guinea:{keywords:["gn","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},guinea_bissau:{keywords:["gw","bissau","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},guyana:{keywords:["gy","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},haiti:{keywords:["ht","flag","nation","country","banner"],char:"\u{1f1ed}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},honduras:{keywords:["hn","flag","nation","country","banner"],char:"\u{1f1ed}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},hong_kong:{keywords:["hong","kong","flag","nation","country","banner"],char:"\u{1f1ed}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},hungary:{keywords:["hu","flag","nation","country","banner"],char:"\u{1f1ed}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},iceland:{keywords:["is","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},india:{keywords:["in","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},indonesia:{keywords:["flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},iran:{keywords:["iran,","islamic","republic","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},iraq:{keywords:["iq","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f6}",fitzpatrick_scale:!1,category:"flags"},ireland:{keywords:["ie","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},isle_of_man:{keywords:["isle","man","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},israel:{keywords:["il","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},it:{keywords:["italy","flag","nation","country","banner"],char:"\u{1f1ee}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},cote_divoire:{keywords:["ivory","coast","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},jamaica:{keywords:["jm","flag","nation","country","banner"],char:"\u{1f1ef}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},jp:{keywords:["japanese","nation","flag","country","banner"],char:"\u{1f1ef}\u{1f1f5}",fitzpatrick_scale:!1,category:"flags"},jersey:{keywords:["je","flag","nation","country","banner"],char:"\u{1f1ef}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},jordan:{keywords:["jo","flag","nation","country","banner"],char:"\u{1f1ef}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},kazakhstan:{keywords:["kz","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},kenya:{keywords:["ke","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},kiribati:{keywords:["ki","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},kosovo:{keywords:["xk","flag","nation","country","banner"],char:"\u{1f1fd}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},kuwait:{keywords:["kw","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},kyrgyzstan:{keywords:["kg","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},laos:{keywords:["lao","democratic","republic","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},latvia:{keywords:["lv","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1fb}",fitzpatrick_scale:!1,category:"flags"},lebanon:{keywords:["lb","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1e7}",fitzpatrick_scale:!1,category:"flags"},lesotho:{keywords:["ls","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},liberia:{keywords:["lr","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},libya:{keywords:["ly","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},liechtenstein:{keywords:["li","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},lithuania:{keywords:["lt","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},luxembourg:{keywords:["lu","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},macau:{keywords:["macao","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},macedonia:{keywords:["macedonia,","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},madagascar:{keywords:["mg","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},malawi:{keywords:["mw","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},malaysia:{keywords:["my","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},maldives:{keywords:["mv","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1fb}",fitzpatrick_scale:!1,category:"flags"},mali:{keywords:["ml","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},malta:{keywords:["mt","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},marshall_islands:{keywords:["marshall","islands","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},martinique:{keywords:["mq","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f6}",fitzpatrick_scale:!1,category:"flags"},mauritania:{keywords:["mr","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},mauritius:{keywords:["mu","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},mayotte:{keywords:["yt","flag","nation","country","banner"],char:"\u{1f1fe}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},mexico:{keywords:["mx","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1fd}",fitzpatrick_scale:!1,category:"flags"},micronesia:{keywords:["micronesia,","federated","states","flag","nation","country","banner"],char:"\u{1f1eb}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},moldova:{keywords:["moldova,","republic","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},monaco:{keywords:["mc","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},mongolia:{keywords:["mn","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},montenegro:{keywords:["me","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},montserrat:{keywords:["ms","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},morocco:{keywords:["ma","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},mozambique:{keywords:["mz","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},myanmar:{keywords:["mm","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},namibia:{keywords:["na","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},nauru:{keywords:["nr","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},nepal:{keywords:["np","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1f5}",fitzpatrick_scale:!1,category:"flags"},netherlands:{keywords:["nl","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},new_caledonia:{keywords:["new","caledonia","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},new_zealand:{keywords:["new","zealand","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},nicaragua:{keywords:["ni","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},niger:{keywords:["ne","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},nigeria:{keywords:["flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},niue:{keywords:["nu","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},norfolk_island:{keywords:["norfolk","island","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},northern_mariana_islands:{keywords:["northern","mariana","islands","flag","nation","country","banner"],char:"\u{1f1f2}\u{1f1f5}",fitzpatrick_scale:!1,category:"flags"},north_korea:{keywords:["north","korea","nation","flag","country","banner"],char:"\u{1f1f0}\u{1f1f5}",fitzpatrick_scale:!1,category:"flags"},norway:{keywords:["no","flag","nation","country","banner"],char:"\u{1f1f3}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},oman:{keywords:["om_symbol","flag","nation","country","banner"],char:"\u{1f1f4}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},pakistan:{keywords:["pk","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},palau:{keywords:["pw","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},palestinian_territories:{keywords:["palestine","palestinian","territories","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},panama:{keywords:["pa","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},papua_new_guinea:{keywords:["papua","new","guinea","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},paraguay:{keywords:["py","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},peru:{keywords:["pe","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},philippines:{keywords:["ph","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},pitcairn_islands:{keywords:["pitcairn","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},poland:{keywords:["pl","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},portugal:{keywords:["pt","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},puerto_rico:{keywords:["puerto","rico","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},qatar:{keywords:["qa","flag","nation","country","banner"],char:"\u{1f1f6}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},reunion:{keywords:["r\xe9union","flag","nation","country","banner"],char:"\u{1f1f7}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},romania:{keywords:["ro","flag","nation","country","banner"],char:"\u{1f1f7}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},ru:{keywords:["russian","federation","flag","nation","country","banner"],char:"\u{1f1f7}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},rwanda:{keywords:["rw","flag","nation","country","banner"],char:"\u{1f1f7}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},st_barthelemy:{keywords:["saint","barth\xe9lemy","flag","nation","country","banner"],char:"\u{1f1e7}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},st_helena:{keywords:["saint","helena","ascension","tristan","cunha","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},st_kitts_nevis:{keywords:["saint","kitts","nevis","flag","nation","country","banner"],char:"\u{1f1f0}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},st_lucia:{keywords:["saint","lucia","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},st_pierre_miquelon:{keywords:["saint","pierre","miquelon","flag","nation","country","banner"],char:"\u{1f1f5}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},st_vincent_grenadines:{keywords:["saint","vincent","grenadines","flag","nation","country","banner"],char:"\u{1f1fb}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},samoa:{keywords:["ws","flag","nation","country","banner"],char:"\u{1f1fc}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},san_marino:{keywords:["san","marino","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},sao_tome_principe:{keywords:["sao","tome","principe","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},saudi_arabia:{keywords:["flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},senegal:{keywords:["sn","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},serbia:{keywords:["rs","flag","nation","country","banner"],char:"\u{1f1f7}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},seychelles:{keywords:["sc","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},sierra_leone:{keywords:["sierra","leone","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},singapore:{keywords:["sg","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},sint_maarten:{keywords:["sint","maarten","dutch","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1fd}",fitzpatrick_scale:!1,category:"flags"},slovakia:{keywords:["sk","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},slovenia:{keywords:["si","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},solomon_islands:{keywords:["solomon","islands","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1e7}",fitzpatrick_scale:!1,category:"flags"},somalia:{keywords:["so","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},south_africa:{keywords:["south","africa","flag","nation","country","banner"],char:"\u{1f1ff}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},south_georgia_south_sandwich_islands:{keywords:["south","georgia","sandwich","islands","flag","nation","country","banner"],char:"\u{1f1ec}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},kr:{keywords:["south","korea","nation","flag","country","banner"],char:"\u{1f1f0}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},south_sudan:{keywords:["south","sd","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},es:{keywords:["spain","flag","nation","country","banner"],char:"\u{1f1ea}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},sri_lanka:{keywords:["sri","lanka","flag","nation","country","banner"],char:"\u{1f1f1}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},sudan:{keywords:["sd","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1e9}",fitzpatrick_scale:!1,category:"flags"},suriname:{keywords:["sr","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},swaziland:{keywords:["sz","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},sweden:{keywords:["se","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},switzerland:{keywords:["ch","flag","nation","country","banner"],char:"\u{1f1e8}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},syria:{keywords:["syrian","arab","republic","flag","nation","country","banner"],char:"\u{1f1f8}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},taiwan:{keywords:["tw","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},tajikistan:{keywords:["tj","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1ef}",fitzpatrick_scale:!1,category:"flags"},tanzania:{keywords:["tanzania,","united","republic","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},thailand:{keywords:["th","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},timor_leste:{keywords:["timor","leste","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1f1}",fitzpatrick_scale:!1,category:"flags"},togo:{keywords:["tg","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},tokelau:{keywords:["tk","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1f0}",fitzpatrick_scale:!1,category:"flags"},tonga:{keywords:["to","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1f4}",fitzpatrick_scale:!1,category:"flags"},trinidad_tobago:{keywords:["trinidad","tobago","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1f9}",fitzpatrick_scale:!1,category:"flags"},tunisia:{keywords:["tn","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},tr:{keywords:["turkey","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1f7}",fitzpatrick_scale:!1,category:"flags"},turkmenistan:{keywords:["flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},turks_caicos_islands:{keywords:["turks","caicos","islands","flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1e8}",fitzpatrick_scale:!1,category:"flags"},tuvalu:{keywords:["flag","nation","country","banner"],char:"\u{1f1f9}\u{1f1fb}",fitzpatrick_scale:!1,category:"flags"},uganda:{keywords:["ug","flag","nation","country","banner"],char:"\u{1f1fa}\u{1f1ec}",fitzpatrick_scale:!1,category:"flags"},ukraine:{keywords:["ua","flag","nation","country","banner"],char:"\u{1f1fa}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},united_arab_emirates:{keywords:["united","arab","emirates","flag","nation","country","banner"],char:"\u{1f1e6}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},uk:{keywords:["united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","UK","english","england","union jack"],char:"\u{1f1ec}\u{1f1e7}",fitzpatrick_scale:!1,category:"flags"},england:{keywords:["flag","english"],char:"\u{1f3f4}\u{e0067}\u{e0062}\u{e0065}\u{e006e}\u{e0067}\u{e007f}",fitzpatrick_scale:!1,category:"flags"},scotland:{keywords:["flag","scottish"],char:"\u{1f3f4}\u{e0067}\u{e0062}\u{e0073}\u{e0063}\u{e0074}\u{e007f}",fitzpatrick_scale:!1,category:"flags"},wales:{keywords:["flag","welsh"],char:"\u{1f3f4}\u{e0067}\u{e0062}\u{e0077}\u{e006c}\u{e0073}\u{e007f}",fitzpatrick_scale:!1,category:"flags"},us:{keywords:["united","states","america","flag","nation","country","banner"],char:"\u{1f1fa}\u{1f1f8}",fitzpatrick_scale:!1,category:"flags"},us_virgin_islands:{keywords:["virgin","islands","us","flag","nation","country","banner"],char:"\u{1f1fb}\u{1f1ee}",fitzpatrick_scale:!1,category:"flags"},uruguay:{keywords:["uy","flag","nation","country","banner"],char:"\u{1f1fa}\u{1f1fe}",fitzpatrick_scale:!1,category:"flags"},uzbekistan:{keywords:["uz","flag","nation","country","banner"],char:"\u{1f1fa}\u{1f1ff}",fitzpatrick_scale:!1,category:"flags"},vanuatu:{keywords:["vu","flag","nation","country","banner"],char:"\u{1f1fb}\u{1f1fa}",fitzpatrick_scale:!1,category:"flags"},vatican_city:{keywords:["vatican","city","flag","nation","country","banner"],char:"\u{1f1fb}\u{1f1e6}",fitzpatrick_scale:!1,category:"flags"},venezuela:{keywords:["ve","bolivarian","republic","flag","nation","country","banner"],char:"\u{1f1fb}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},vietnam:{keywords:["viet","nam","flag","nation","country","banner"],char:"\u{1f1fb}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},wallis_futuna:{keywords:["wallis","futuna","flag","nation","country","banner"],char:"\u{1f1fc}\u{1f1eb}",fitzpatrick_scale:!1,category:"flags"},western_sahara:{keywords:["western","sahara","flag","nation","country","banner"],char:"\u{1f1ea}\u{1f1ed}",fitzpatrick_scale:!1,category:"flags"},yemen:{keywords:["ye","flag","nation","country","banner"],char:"\u{1f1fe}\u{1f1ea}",fitzpatrick_scale:!1,category:"flags"},zambia:{keywords:["zm","flag","nation","country","banner"],char:"\u{1f1ff}\u{1f1f2}",fitzpatrick_scale:!1,category:"flags"},zimbabwe:{keywords:["zw","flag","nation","country","banner"],char:"\u{1f1ff}\u{1f1fc}",fitzpatrick_scale:!1,category:"flags"},united_nations:{keywords:["un","flag","banner"],char:"\u{1f1fa}\u{1f1f3}",fitzpatrick_scale:!1,category:"flags"},pirate_flag:{keywords:["skull","crossbones","flag","banner"],char:"\u{1f3f4}\u200d\u2620\ufe0f",fitzpatrick_scale:!1,category:"flags"}}); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/emoticons/plugin.min.js b/src/main/webapp/content/tinymce/plugins/emoticons/plugin.min.js new file mode 100644 index 0000000..23fa192 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/emoticons/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>e=>t===e,o=e(null),n=e(void 0),s=()=>{},r=()=>!1;class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return null==t?a.none():a.some(t)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const i=(t,e)=>{const o=t.length,n=new Array(o);for(let s=0;s{let e=t;return{get:()=>e,set:t=>{e=t}}},c=Object.keys,u=Object.hasOwnProperty,g=(t,e)=>{const o=c(t);for(let n=0,s=o.length;nu.call(t,e),m=(h=(t,e)=>e,(...t)=>{if(0===t.length)throw new Error("Can't merge zero objects");const e={};for(let o=0;o{const t=(t=>{const e=l(a.none()),o=()=>e.get().each(t);return{clear:()=>{o(),e.set(a.none())},isSet:()=>e.get().isSome(),get:()=>e.get(),set:t=>{o(),e.set(a.some(t))}}})(s);return{...t,on:e=>t.get().each(e)}},y=(t,e,o=0,s)=>{const r=t.indexOf(e,o);return-1!==r&&(!!n(s)||r+e.length<=s)};var v=tinymce.util.Tools.resolve("tinymce.Resource");const f=t=>e=>e.options.get(t),b=f("emoticons_database"),w=f("emoticons_database_url"),j=f("emoticons_database_id"),C=f("emoticons_append"),_=f("emoticons_images_url"),A="All",k={symbols:"Symbols",people:"People",animals_and_nature:"Animals and Nature",food_and_drink:"Food and Drink",activity:"Activity",travel_and_places:"Travel and Places",objects:"Objects",flags:"Flags",user:"User Defined"},O=(t,e)=>d(t,e)?t[e]:e,x=t=>{const e=C(t);return o=t=>({keywords:[],category:"user",...t}),((t,e)=>{const o={};return g(t,((t,n)=>{const s=e(t,n);o[s.k]=s.v})),o})(e,((t,e)=>({k:e,v:o(t)})));var o},E=(t,e)=>y(t.title.toLowerCase(),e)||(t=>{for(let n=0,s=t.length;n{const n=[],s=e.toLowerCase(),a=o.fold((()=>r),(t=>e=>e>=t));for(let o=0;o{const n={pattern:"",results:L(e.listAll(),"",a.some(300))},s=l(A),r=(t=>{let e=null;const n=()=>{o(e)||(clearTimeout(e),e=null)};return{cancel:n,throttle:(...o)=>{n(),e=setTimeout((()=>{e=null,t.apply(null,o)}),200)}}})((t=>{(t=>{const o=t.getData(),n=s.get(),r=e.listCategory(n),i=L(r,o[S],n===A?a.some(300):a.none());t.setData({results:i})})(t)})),c={label:"Search",type:"input",name:S},u={type:"collection",name:"results"},g=()=>({title:"Emojis",size:"normal",body:{type:"tabpanel",tabs:i(e.listCategories(),(t=>({title:t,name:t,items:[c,u]})))},initialData:n,onTabChange:(t,e)=>{s.set(e.newTabName),r.throttle(t)},onChange:r.throttle,onAction:(e,o)=>{"results"===o.name&&(((t,e)=>{t.insertContent(e)})(t,o.value),e.close())},buttons:[{type:"cancel",text:"Close",primary:!0}]}),d=t.windowManager.open(g());d.focus(S),e.hasLoaded()||(d.block("Loading emojis..."),e.waitForLoad().then((()=>{d.redial(g()),r.throttle(d),d.focus(S),d.unblock()})).catch((t=>{d.redial({title:"Emojis",body:{type:"panel",items:[{type:"alertbanner",level:"error",icon:"warning",text:"Could not load emojis"}]},buttons:[{type:"cancel",text:"Close",primary:!0}],initialData:{pattern:"",results:[]}}),d.focus(S),d.unblock()})))},T=t=>e=>{const o=()=>{e.setEnabled(t.selection.isEditable())};return t.on("NodeChange",o),o(),()=>{t.off("NodeChange",o)}};t.add("emoticons",((t,e)=>{((t,e)=>{const o=t.options.register;o("emoticons_database",{processor:"string",default:"emojis"}),o("emoticons_database_url",{processor:"string",default:`${e}/js/${b(t)}${t.suffix}.js`}),o("emoticons_database_id",{processor:"string",default:"tinymce.plugins.emoticons"}),o("emoticons_append",{processor:"object",default:{}}),o("emoticons_images_url",{processor:"string",default:"https://cdnjs.cloudflare.com/ajax/libs/twemoji/15.1.0/72x72/"})})(t,e);const o=((t,e,o)=>{const n=p(),s=p(),r=_(t),i=t=>{return o="=4&&e.substr(0,4)===o?t.char.replace(/src="([^"]+)"/,((t,e)=>`src="${r}${e}"`)):t.char;var e,o};t.on("init",(()=>{v.load(o,e).then((e=>{const o=x(t);(t=>{const e={},o=[];g(t,((t,n)=>{const s={title:n,keywords:t.keywords,char:i(t),category:O(k,t.category)},r=void 0!==e[s.category]?e[s.category]:[];e[s.category]=r.concat([s]),o.push(s)})),n.set(e),s.set(o)})(m(e,o))}),(t=>{console.log(`Failed to load emojis: ${t}`),n.set({}),s.set([])}))}));const l=()=>s.get().getOr([]),u=()=>n.isSet()&&s.isSet();return{listCategories:()=>[A].concat(c(n.get().getOr({}))),hasLoaded:u,waitForLoad:()=>u()?Promise.resolve(!0):new Promise(((t,o)=>{let n=15;const s=setInterval((()=>{u()?(clearInterval(s),t(!0)):(n--,n<0&&(console.log("Could not load emojis from url: "+e),clearInterval(s),o(!1)))}),100)})),listAll:l,listCategory:t=>t===A?l():n.get().bind((e=>a.from(e[t]))).getOr([])}})(t,w(t),j(t));return((t,e)=>{t.addCommand("mceEmoticons",(()=>N(t,e)))})(t,o),(t=>{const e=()=>t.execCommand("mceEmoticons");t.ui.registry.addButton("emoticons",{tooltip:"Emojis",icon:"emoji",onAction:e,onSetup:T(t)}),t.ui.registry.addMenuItem("emoticons",{text:"Emojis...",icon:"emoji",onAction:e,onSetup:T(t)})})(t),((t,e)=>{t.ui.registry.addAutocompleter("emoticons",{trigger:":",columns:"auto",minChars:2,fetch:(t,o)=>e.waitForLoad().then((()=>{const n=e.listAll();return L(n,t,a.some(o))})),onAction:(e,o,n)=>{t.selection.setRng(o),t.insertContent(n),e.hide()}})})(t,o),(t=>{t.on("PreInit",(()=>{t.parser.addAttributeFilter("data-emoticon",(t=>{(t=>{for(let o=0,n=t.length;oo.waitForLoad().then((()=>o.listAll()))}}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/fullscreen/plugin.min.js b/src/main/webapp/content/tinymce/plugins/fullscreen/plugin.min.js new file mode 100644 index 0000000..37f4522 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/fullscreen/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";const e=e=>{let t=e;return{get:()=>t,set:e=>{t=e}}};var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const n=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=r=e,(o=String).prototype.isPrototypeOf(n)||(null===(s=r.constructor)||void 0===s?void 0:s.name)===o.name)?"string":t;var n,r,o,s})(t)===e,r=e=>t=>typeof t===e,o=e=>t=>e===t,s=n("string"),i=n("object"),l=n("array"),a=o(null),c=r("boolean"),u=o(void 0),d=e=>!(e=>null==e)(e),m=r("function"),h=r("number"),g=()=>{},p=e=>()=>e;function f(e,...t){return(...n)=>{const r=t.concat(n);return e.apply(null,r)}}const v=p(!1),w=p(!0);class y{constructor(e,t){this.tag=e,this.value=t}static some(e){return new y(!0,e)}static none(){return y.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?y.some(e(this.value)):y.none()}bind(e){return this.tag?e(this.value):y.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:y.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return d(e)?y.some(e):y.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}y.singletonNone=new y(!1);const b=Array.prototype.push,S=(e,t)=>{const n=e.length,r=new Array(n);for(let o=0;o{for(let n=0,r=e.length;n{const n=[];for(let r=0,o=e.length;r((e,t,n)=>{for(let r=0,o=e.length;r{const n=e(y.none()),r=()=>n.get().each(t);return{clear:()=>{r(),n.set(y.none())},isSet:()=>n.get().isSome(),get:()=>n.get(),set:e=>{r(),n.set(y.some(e))}}},k=()=>O((e=>e.unbind())),T=Object.keys,C="undefined"!=typeof window?window:Function("return this;")(),A=(e,t)=>((e,t)=>{let n=null!=t?t:C;for(let t=0;t{const t=A("ownerDocument.defaultView",e);return i(e)&&((e=>((e,t)=>{const n=((e,t)=>A(e,t))(e,t);if(null==n)throw new Error(e+" not available on this browser");return n})("HTMLElement",e))(t).prototype.isPrototypeOf(e)||/^HTML\w*Element$/.test(R(e).constructor.name))},M=e=>t=>(e=>e.dom.nodeType)(t)===e,P=M(1),D=M(3),N=M(11),H=(e,t)=>{const n=e.dom.getAttribute(t);return null===n?void 0:n},V=(e,t)=>{e.dom.removeAttribute(t)},W=(e,t,n=0,r)=>{const o=e.indexOf(t,n);return-1!==o&&(!!u(r)||o+t.length<=r)},q=e=>void 0!==e.style&&m(e.style.getPropertyValue),B=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},I=B,j=(e,t)=>{const n=e.dom;if(1!==n.nodeType)return!1;{const e=n;if(void 0!==e.matches)return e.matches(t);if(void 0!==e.msMatchesSelector)return e.msMatchesSelector(t);if(void 0!==e.webkitMatchesSelector)return e.webkitMatchesSelector(t);if(void 0!==e.mozMatchesSelector)return e.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")}},_=e=>I(e.dom.ownerDocument),z=e=>S(e.dom.childNodes,I),K=e=>{const t=(e=>I(e.dom.getRootNode()))(e);return N(n=t)&&d(n.dom.host)?y.some(t):y.none();var n},$=e=>I(e.dom.host),U=e=>{const t=D(e)?e.dom.parentNode:e.dom;if(null==t||null===t.ownerDocument)return!1;const n=t.ownerDocument;return K(I(t)).fold((()=>n.body.contains(t)),(r=U,o=$,e=>r(o(e))));var r,o},X=(e,t,n)=>{if(!s(n))throw console.error("Invalid call to CSS.set. Property ",t,":: Value ",n,":: Element ",e),new Error("CSS value must be a string: "+n);q(e)&&e.style.setProperty(t,n)},Y=(e,t,n)=>{const r=e.dom;X(r,t,n)},G=(e,t)=>{const n=e.dom;((e,t)=>{const n=T(e);for(let r=0,o=n.length;r{X(n,t,e)}))},J=(e,t)=>{const n=e.dom,r=window.getComputedStyle(n).getPropertyValue(t);return""!==r||U(e)?r:Q(n,t)},Q=(e,t)=>q(e)?e.style.getPropertyValue(t):"",Z=e=>{const t=I((e=>{if(d(e.target)){const t=I(e.target);if(P(t)&&d(t.dom.shadowRoot)&&e.composed&&e.composedPath){const t=e.composedPath();if(t)return(e=>0e.stopPropagation(),r=()=>e.preventDefault(),o=(s=r,i=n,(...e)=>s(i.apply(null,e)));var s,i;return((e,t,n,r,o,s,i)=>({target:e,x:t,y:n,stop:r,prevent:o,kill:s,raw:i}))(t,e.clientX,e.clientY,n,r,o,e)},ee=(e,t,n,r)=>{e.dom.removeEventListener(t,n,r)},te=w,ne=(e,t,n)=>((e,t,n,r)=>((e,t,n,r,o)=>{const s=((e,t)=>n=>{e(n)&&t(Z(n))})(n,r);return e.dom.addEventListener(t,s,o),{unbind:f(ee,e,t,s,o)}})(e,t,n,r,!1))(e,t,te,n),re=()=>oe(0,0),oe=(e,t)=>({major:e,minor:t}),se={nu:oe,detect:(e,t)=>{const n=String(t).toLowerCase();return 0===e.length?re():((e,t)=>{const n=((e,t)=>{for(let n=0;nNumber(t.replace(n,"$"+e));return oe(r(1),r(2))})(e,n)},unknown:re},ie=(e,t)=>{const n=String(t).toLowerCase();return F(e,(e=>e.search(n)))},le=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,ae=e=>t=>W(t,e),ce=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:e=>W(e,"edge/")&&W(e,"chrome")&&W(e,"safari")&&W(e,"applewebkit")},{name:"Chromium",brand:"Chromium",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,le],search:e=>W(e,"chrome")&&!W(e,"chromeframe")},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:e=>W(e,"msie")||W(e,"trident")},{name:"Opera",versionRegexes:[le,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:ae("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:ae("firefox")},{name:"Safari",versionRegexes:[le,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:e=>(W(e,"safari")||W(e,"mobile/"))&&W(e,"applewebkit")}],ue=[{name:"Windows",search:ae("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:e=>W(e,"iphone")||W(e,"ipad"),versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:ae("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"macOS",search:ae("mac os x"),versionRegexes:[/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:ae("linux"),versionRegexes:[]},{name:"Solaris",search:ae("sunos"),versionRegexes:[]},{name:"FreeBSD",search:ae("freebsd"),versionRegexes:[]},{name:"ChromeOS",search:ae("cros"),versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/]}],de={browsers:p(ce),oses:p(ue)},me="Edge",he="Chromium",ge="Opera",pe="Firefox",fe="Safari",ve=e=>{const t=e.current,n=e.version,r=e=>()=>t===e;return{current:t,version:n,isEdge:r(me),isChromium:r(he),isIE:r("IE"),isOpera:r(ge),isFirefox:r(pe),isSafari:r(fe)}},we=()=>ve({current:void 0,version:se.unknown()}),ye=ve,be=(p(me),p(he),p("IE"),p(ge),p(pe),p(fe),"Windows"),Se="Android",xe="Linux",Ee="macOS",Fe="Solaris",Oe="FreeBSD",ke="ChromeOS",Te=e=>{const t=e.current,n=e.version,r=e=>()=>t===e;return{current:t,version:n,isWindows:r(be),isiOS:r("iOS"),isAndroid:r(Se),isMacOS:r(Ee),isLinux:r(xe),isSolaris:r(Fe),isFreeBSD:r(Oe),isChromeOS:r(ke)}},Ce=()=>Te({current:void 0,version:se.unknown()}),Ae=Te,Re=(p(be),p("iOS"),p(Se),p(xe),p(Ee),p(Fe),p(Oe),p(ke),(e,t,n)=>{const r=de.browsers(),o=de.oses(),s=t.bind((e=>((e,t)=>((e,t)=>{for(let n=0;n{const n=t.brand.toLowerCase();return F(e,(e=>{var t;return n===(null===(t=e.brand)||void 0===t?void 0:t.toLowerCase())})).map((e=>({current:e.name,version:se.nu(parseInt(t.version,10),0)})))})))(r,e))).orThunk((()=>((e,t)=>ie(e,t).map((e=>{const n=se.detect(e.versionRegexes,t);return{current:e.name,version:n}})))(r,e))).fold(we,ye),i=((e,t)=>ie(e,t).map((e=>{const n=se.detect(e.versionRegexes,t);return{current:e.name,version:n}})))(o,e).fold(Ce,Ae),l=((e,t,n,r)=>{const o=e.isiOS()&&!0===/ipad/i.test(n),s=e.isiOS()&&!o,i=e.isiOS()||e.isAndroid(),l=i||r("(pointer:coarse)"),a=o||!s&&i&&r("(min-device-width:768px)"),c=s||i&&!a,u=t.isSafari()&&e.isiOS()&&!1===/safari/i.test(n),d=!c&&!a&&!u;return{isiPad:p(o),isiPhone:p(s),isTablet:p(a),isPhone:p(c),isTouch:p(l),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:p(u),isDesktop:p(d)}})(i,s,e,n);return{browser:s,os:i,deviceType:l}}),Le=e=>window.matchMedia(e).matches;let Me=(e=>{let t,n=!1;return(...r)=>(n||(n=!0,t=e.apply(null,r)),t)})((()=>Re(window.navigator.userAgent,y.from(window.navigator.userAgentData),Le)));const Pe=(e,t)=>({left:e,top:t,translate:(n,r)=>Pe(e+n,t+r)}),De=Pe,Ne=e=>{const t=void 0===e?window:e;return Me().browser.isFirefox()?y.none():y.from(t.visualViewport)},He=(e,t,n,r)=>({x:e,y:t,width:n,height:r,right:e+n,bottom:t+r}),Ve=e=>{const t=void 0===e?window:e,n=t.document,r=(e=>{const t=void 0!==e?e.dom:document,n=t.body.scrollLeft||t.documentElement.scrollLeft,r=t.body.scrollTop||t.documentElement.scrollTop;return De(n,r)})(I(n));return Ne(t).fold((()=>{const e=t.document.documentElement,n=e.clientWidth,o=e.clientHeight;return He(r.left,r.top,n,o)}),(e=>He(Math.max(e.pageLeft,r.left),Math.max(e.pageTop,r.top),e.width,e.height)))},We=(e,t,n)=>Ne(n).map((n=>{const r=e=>t(Z(e));return n.addEventListener(e,r),{unbind:()=>n.removeEventListener(e,r)}})).getOrThunk((()=>({unbind:g})));var qe=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),Be=tinymce.util.Tools.resolve("tinymce.Env");const Ie=(e,t)=>{e.dispatch("FullscreenStateChanged",{state:t}),e.dispatch("ResizeEditor")},je=e=>e.options.get("fullscreen_native");const _e=e=>{return e.dom===(void 0!==(t=_(e).dom).fullscreenElement?t.fullscreenElement:void 0!==t.msFullscreenElement?t.msFullscreenElement:void 0!==t.webkitFullscreenElement?t.webkitFullscreenElement:null);var t},ze=(e,t,n)=>((e,t,n)=>E(((e,t)=>{const n=m(t)?t:v;let r=e.dom;const o=[];for(;null!==r.parentNode&&void 0!==r.parentNode;){const e=r.parentNode,t=I(e);if(o.push(t),!0===n(t))break;r=e}return o})(e,n),t))(e,(e=>j(e,t)),n),Ke=(e,t)=>(e=>{return E((e=>y.from(e.dom.parentNode).map(I))(n=e).map(z).map((e=>E(e,(e=>{return t=e,!(n.dom===t.dom);var t})))).getOr([]),(e=>j(e,t)));var n})(e),$e="data-ephox-mobile-fullscreen-style",Ue="position:absolute!important;",Xe="top:0!important;left:0!important;margin:0!important;padding:0!important;width:100%!important;height:100%!important;overflow:visible!important;",Ye=Be.os.isAndroid(),Ge=(e,t,n)=>{const r=t=>n=>{const r=H(n,"style"),o=void 0===r?"no-styles":r.trim();o!==t&&(((e,t,n)=>{((e,t,n)=>{if(!(s(n)||c(n)||h(n)))throw console.error("Invalid call to Attribute.set. Key ",t,":: Value ",n,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,n+"")})(e.dom,t,n)})(n,$e,o),G(n,e.parseStyle(t)))},o=ze(t,"*"),i=(e=>{const t=[];for(let n=0,r=e.length;nKe(e,"*:not(.tox-silver-sink)")))),a=(e=>{const t=J(e,"background-color");return void 0!==t&&""!==t?"background-color:"+t+"!important":"background-color:rgb(255,255,255)!important;"})(n);x(i,r("display:none!important;")),x(o,r(Ue+Xe+a)),r((!0===Ye?"":Ue)+Xe+a)(t)},Je=qe.DOM,Qe=Ne().fold((()=>({bind:g,unbind:g})),(e=>{const t=(()=>{const e=O(g);return{...e,on:t=>e.get().each(t)}})(),n=k(),r=k(),o=(e=>{let t=null;return{cancel:()=>{a(t)||(clearTimeout(t),t=null)},throttle:(...n)=>{a(t)&&(t=setTimeout((()=>{t=null,e.apply(null,n)}),50))}}})((()=>{document.body.scrollTop=0,document.documentElement.scrollTop=0,window.requestAnimationFrame((()=>{t.on((t=>G(t,{top:e.offsetTop+"px",left:e.offsetLeft+"px",height:e.height+"px",width:e.width+"px"})))}))}));return{bind:e=>{t.set(e),o.throttle(),n.set(We("resize",o.throttle)),r.set(We("scroll",o.throttle))},unbind:()=>{t.on((()=>{n.clear(),r.clear()})),t.clear()}}})),Ze=(e,t)=>{const n=document.body,r=document.documentElement,o=e.getContainer(),s=I(o),i=(l=s,y.from(l.dom.nextSibling).map(I)).filter((e=>(e=>P(e)&&L(e.dom))(e)&&(e=>(e=>void 0!==e.dom.classList)(e)&&e.dom.classList.contains("tox-silver-sink"))(e)));var l;const a=(e=>{const t=I(e.getElement());return K(t).map($).getOrThunk((()=>(e=>{const t=e.dom.body;if(null==t)throw new Error("Body is not available yet");return I(t)})(_(t))))})(e),c=t.get(),u=I(e.getBody()),d=Be.deviceType.isTouch(),m=o.style,h=e.iframeElement,g=null==h?void 0:h.style,p=e=>{e(n,"tox-fullscreen"),e(r,"tox-fullscreen"),e(o,"tox-fullscreen"),K(s).map((e=>$(e).dom)).each((t=>{e(t,"tox-fullscreen"),e(t,"tox-shadowhost")}))},f=()=>{d&&(e=>{const t=(e=>{const t=document;return 1!==(n=t).nodeType&&9!==n.nodeType&&11!==n.nodeType||0===n.childElementCount?[]:S(t.querySelectorAll(e),I);var n})("["+$e+"]");x(t,(t=>{const n=H(t,$e);n&&"no-styles"!==n?G(t,e.parseStyle(n)):V(t,"style"),V(t,$e)}))})(e.dom),p(Je.removeClass),Qe.unbind(),y.from(t.get()).each((e=>e.fullscreenChangeHandler.unbind()))};if(c)c.fullscreenChangeHandler.unbind(),je(e)&&_e(a)&&(e=>{const t=e.dom;t.exitFullscreen?t.exitFullscreen():t.msExitFullscreen?t.msExitFullscreen():t.webkitCancelFullScreen&&t.webkitCancelFullScreen()})(_(a)),g.width=c.iframeWidth,g.height=c.iframeHeight,m.width=c.containerWidth,m.height=c.containerHeight,m.top=c.containerTop,m.left=c.containerLeft,w=i,b=c.sinkCssPosition,E=(e,t)=>{Y(e,"position",t)},w.isSome()&&b.isSome()?y.some(E(w.getOrDie(),b.getOrDie())):y.none(),f(),v=c.scrollPos,window.scrollTo(v.x,v.y),t.set(null),Ie(e,!1),e.off("remove",f);else{const n=ne(_(a),void 0!==document.fullscreenElement?"fullscreenchange":void 0!==document.msFullscreenElement?"MSFullscreenChange":void 0!==document.webkitFullscreenElement?"webkitfullscreenchange":"fullscreenchange",(n=>{je(e)&&(_e(a)||null===t.get()||Ze(e,t))})),r={scrollPos:Ve(window),containerWidth:m.width,containerHeight:m.height,containerTop:m.top,containerLeft:m.left,iframeWidth:g.width,iframeHeight:g.height,fullscreenChangeHandler:n,sinkCssPosition:i.map((e=>J(e,"position")))};d&&Ge(e.dom,s,u),g.width=g.height="100%",m.width=m.height="",p(Je.addClass),i.each((e=>{Y(e,"position","fixed")})),Qe.bind(s),e.on("remove",f),t.set(r),je(e)&&(e=>{const t=e.dom;t.requestFullscreen?t.requestFullscreen():t.msRequestFullscreen?t.msRequestFullscreen():t.webkitRequestFullScreen&&t.webkitRequestFullScreen()})(a),Ie(e,!0)}var v,w,b,E};var et=tinymce.util.Tools.resolve("tinymce.util.VK");const tt=(e,t)=>n=>{n.setActive(null!==t.get());const r=e=>n.setActive(e.state);return e.on("FullscreenStateChanged",r),()=>e.off("FullscreenStateChanged",r)};t.add("fullscreen",(t=>{const n=e(null);return t.inline||((e=>{(0,e.options.register)("fullscreen_native",{processor:"boolean",default:!1})})(t),((e,t)=>{e.addCommand("mceFullScreen",(()=>{Ze(e,t)}))})(t,n),((e,t)=>{const n=()=>e.execCommand("mceFullScreen");e.ui.registry.addToggleMenuItem("fullscreen",{text:"Fullscreen",icon:"fullscreen",shortcut:"Meta+Shift+F",onAction:n,onSetup:tt(e,t),context:"any"}),e.ui.registry.addToggleButton("fullscreen",{tooltip:"Fullscreen",icon:"fullscreen",onAction:n,onSetup:tt(e,t),shortcut:"Meta+Shift+F",context:"any"})})(t,n),((e,t)=>{e.on("init",(()=>{e.on("keydown",(e=>{e.keyCode!==et.TAB||e.metaKey||e.ctrlKey||!t.get()||e.preventDefault()}))}))})(t,n),t.addShortcut("Meta+Shift+F","","mceFullScreen")),(e=>({isFullscreen:()=>null!==e.get()}))(n)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ar.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ar.js new file mode 100644 index 0000000..e2cf02f --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ar.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.ar', +'

    بدء التنقل بواسطة لوحة المفاتيح

    \n' + + '\n' + + '
    \n' + + '
    التركيز على شريط القوائم
    \n' + + '
    نظاما التشغيل Windows أو Linux: Alt + F9
    \n' + + '
    نظام التشغيل macOS: ⌥F9
    \n' + + '
    التركيز على شريط الأدوات
    \n' + + '
    نظاما التشغيل Windows أو Linux: Alt + F10
    \n' + + '
    نظام التشغيل macOS: ⌥F10
    \n' + + '
    التركيز على التذييل
    \n' + + '
    نظاما التشغيل Windows أو Linux: Alt + F11
    \n' + + '
    نظام التشغيل macOS: ⌥F11
    \n' + + '
    تركيز الإشعارات
    \n' + + '
    نظاما التشغيل Windows أو Linux: Alt + F12
    \n' + + '
    نظام التشغيل macOS: ⌥F12
    \n' + + '
    التركيز على شريط أدوات السياق
    \n' + + '
    أنظمة التشغيل Windows أو Linux أو macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    سيبدأ التنقل عند عنصر واجهة المستخدم الأول، والذي سيتم تمييزه أو تسطيره في حالة العنصر الأول في\n' + + ' مسار عنصر التذييل.

    \n' + + '\n' + + '

    التنقل بين أقسام واجهة المستخدم

    \n' + + '\n' + + '

    للانتقال من أحد أقسام واجهة المستخدم إلى القسم التالي، اضغط على Tab.

    \n' + + '\n' + + '

    للانتقال من أحد أقسام واجهة المستخدم إلى القسم السابق، اضغط على Shift+Tab.

    \n' + + '\n' + + '

    ترتيب علامات Tab لأقسام واجهة المستخدم هذه هو:

    \n' + + '\n' + + '
      \n' + + '
    1. شريط القوائم
    2. \n' + + '
    3. كل مجموعة شريط الأدوات
    4. \n' + + '
    5. الشريط الجانبي
    6. \n' + + '
    7. مسار العنصر في التذييل
    8. \n' + + '
    9. زر تبديل عدد الكلمات في التذييل
    10. \n' + + '
    11. رابط إدراج العلامة التجارية في التذييل
    12. \n' + + '
    13. مؤشر تغيير حجم المحرر في التذييل
    14. \n' + + '
    \n' + + '\n' + + '

    إذا لم يكن قسم واجهة المستخدم موجودًا، فسيتم تخطيه.

    \n' + + '\n' + + '

    إذا كان التذييل يحتوي على التركيز على ‏‫التنقل بواسطة لوحة المفاتيح، ولا يوجد شريط جانبي مرئي، فإن الضغط على Shift+Tab\n' + + ' ينقل التركيز إلى مجموعة شريط الأدوات الأولى، وليس الأخيرة.

    \n' + + '\n' + + '

    التنقل بين أقسام واجهة المستخدم

    \n' + + '\n' + + '

    للانتقال من أحد عناصر واجهة المستخدم إلى العنصر التالي، اضغط على مفتاح السهم المناسب.

    \n' + + '\n' + + '

    مفتاحا السهمين اليسار‎ واليمين‎

    \n' + + '\n' + + '
      \n' + + '
    • التنقل بين القوائم في شريط القوائم.
    • \n' + + '
    • فتح قائمة فرعية في القائمة.
    • \n' + + '
    • التنقل بين الأزرار في مجموعة شريط الأدوات.
    • \n' + + '
    • التنقل بين العناصر في مسار عنصر التذييل.
    • \n' + + '
    \n' + + '\n' + + '

    مفتاحا السهمين لأسفل‎ ولأعلى‎

    \n' + + '\n' + + '
      \n' + + '
    • التنقل بين عناصر القائمة في القائمة.
    • \n' + + '
    • التنقل بين العناصر في قائمة شريط الأدوات المنبثقة.
    • \n' + + '
    \n' + + '\n' + + '

    دورة مفاتيح الأسهم‎ داخل قسم واجهة المستخدم التي تم التركيز عليها.

    \n' + + '\n' + + '

    لإغلاق قائمة مفتوحة أو قائمة فرعية مفتوحة أو قائمة منبثقة مفتوحة، اضغط على مفتاح Esc.

    \n' + + '\n' + + '

    إذا كان التركيز الحالي على "الجزء العلوي" من قسم معين لواجهة المستخدم، فإن الضغط على مفتاح Esc يؤدي أيضًا إلى الخروج\n' + + ' من التنقل بواسطة لوحة المفاتيح بالكامل.

    \n' + + '\n' + + '

    تنفيذ عنصر قائمة أو زر شريط أدوات

    \n' + + '\n' + + '

    عندما يتم تمييز عنصر القائمة المطلوب أو زر شريط الأدوات، اضغط على زر Return، أو Enter،\n' + + ' أو مفتاح المسافة لتنفيذ العنصر.

    \n' + + '\n' + + '

    التنقل في مربعات الحوار غير المبوبة

    \n' + + '\n' + + '

    في مربعات الحوار غير المبوبة، يتم التركيز على المكون التفاعلي الأول عند فتح مربع الحوار.

    \n' + + '\n' + + '

    التنقل بين مكونات الحوار التفاعلي بالضغط على زر Tab أو Shift+Tab.

    \n' + + '\n' + + '

    التنقل في مربعات الحوار المبوبة

    \n' + + '\n' + + '

    في مربعات الحوار المبوبة، يتم التركيز على الزر الأول في قائمة علامات التبويب عند فتح مربع الحوار.

    \n' + + '\n' + + '

    التنقل بين المكونات التفاعلية لعلامة التبويب لمربع الحوار هذه بالضغط على زر Tab أو\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    التبديل إلى علامة تبويب أخرى لمربع الحوار من خلال التركيز على قائمة علامة التبويب ثم الضغط على زر السهم المناسب\n' + + ' مفتاح للتنقل بين علامات التبويب المتاحة.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/bg_BG.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/bg_BG.js new file mode 100644 index 0000000..09eacf3 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/bg_BG.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.bg_BG', +'

    Начало на навигацията с клавиатурата

    \n' + + '\n' + + '
    \n' + + '
    Фокусиране върху лентата с менюта
    \n' + + '
    Windows или Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Фокусиране върху лентата с инструменти
    \n' + + '
    Windows или Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Фокусиране върху долния колонтитул
    \n' + + '
    Windows или Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Фокусиране на известието
    \n' + + '
    Windows или Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Фокусиране върху контекстуалната лента с инструменти
    \n' + + '
    Windows, Linux или macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Навигацията ще започне с първия елемент на ПИ, който ще бъде маркиран или подчертан в случая на първия елемент в\n' + + ' пътя до елемента в долния колонтитул.

    \n' + + '\n' + + '

    Навигиране между раздели на ПИ

    \n' + + '\n' + + '

    За да преминете от един раздел на ПИ към следващия, натиснете Tab.

    \n' + + '\n' + + '

    За да преминете от един раздел на ПИ към предишния, натиснете Shift+Tab.

    \n' + + '\n' + + '

    Редът за обхождане с табулация на тези раздели на ПИ е:

    \n' + + '\n' + + '
      \n' + + '
    1. Лентата с менюта
    2. \n' + + '
    3. Всяка група на лентата с инструменти
    4. \n' + + '
    5. Страничната лента
    6. \n' + + '
    7. Пътят до елемента в долния колонтитул
    8. \n' + + '
    9. Бутонът за превключване на броя на думите в долния колонтитул
    10. \n' + + '
    11. Връзката за търговска марка в долния колонтитул
    12. \n' + + '
    13. Манипулаторът за преоразмеряване на редактора в долния колонтитул
    14. \n' + + '
    \n' + + '\n' + + '

    Ако някой раздел на ПИ липсва, той се пропуска.

    \n' + + '\n' + + '

    Ако долният колонтитул има фокус за навигация с клавиатурата и няма странична лента, натискането на Shift+Tab\n' + + ' премества фокуса към първата група на лентата с инструменти, а не към последната.

    \n' + + '\n' + + '

    Навигиране в разделите на ПИ

    \n' + + '\n' + + '

    За да преминете от един елемент на ПИ към следващия, натиснете съответния клавиш със стрелка.

    \n' + + '\n' + + '

    С клавишите със стрелка наляво и надясно

    \n' + + '\n' + + '
      \n' + + '
    • се придвижвате между менютата в лентата с менюто;
    • \n' + + '
    • отваряте подменю в меню;
    • \n' + + '
    • се придвижвате между бутоните в група на лентата с инструменти;
    • \n' + + '
    • се придвижвате между елементи в пътя до елемент в долния колонтитул.
    • \n' + + '
    \n' + + '\n' + + '

    С клавишите със стрелка надолу и нагоре

    \n' + + '\n' + + '
      \n' + + '
    • се придвижвате между елементите от менюто в дадено меню;
    • \n' + + '
    • се придвижвате между елементите в изскачащо меню на лентата с инструменти.
    • \n' + + '
    \n' + + '\n' + + '

    Клавишите със стрелки се придвижват в рамките на фокусирания раздел на ПИ.

    \n' + + '\n' + + '

    За да затворите отворено меню, подменю или изскачащо меню, натиснете клавиша Esc.

    \n' + + '\n' + + '

    Ако текущият фокус е върху „горната част“ на конкретен раздел на ПИ, натискането на клавиша Esc също излиза\n' + + ' напълно от навигацията с клавиатурата.

    \n' + + '\n' + + '

    Изпълнение на елемент от менюто или бутон от лентата с инструменти

    \n' + + '\n' + + '

    Когато желаният елемент от менюто или бутон от лентата с инструменти е маркиран, натиснете Return, Enter\n' + + ' или клавиша за интервал, за да изпълните елемента.

    \n' + + '\n' + + '

    Навигиране в диалогови прозорци без раздели

    \n' + + '\n' + + '

    В диалоговите прозорци без раздели първият интерактивен компонент се фокусира, когато се отвори диалоговият прозорец.

    \n' + + '\n' + + '

    Навигирайте между интерактивните компоненти на диалоговия прозорец, като натиснете Tab или Shift+Tab.

    \n' + + '\n' + + '

    Навигиране в диалогови прозорци с раздели

    \n' + + '\n' + + '

    В диалоговите прозорци с раздели първият бутон в менюто с раздели се фокусира, когато се отвори диалоговият прозорец.

    \n' + + '\n' + + '

    Навигирайте между интерактивните компоненти на този диалогов раздел, като натиснете Tab или\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Превключете към друг диалогов раздел, като фокусирате върху менюто с раздели и след това натиснете съответния клавиш със стрелка,\n' + + ' за да преминете през наличните раздели.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ca.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ca.js new file mode 100644 index 0000000..996e29c --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ca.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.ca', +'

    Inici de la navegació amb el teclat

    \n' + + '\n' + + '
    \n' + + '
    Enfocar la barra de menús
    \n' + + '
    Windows o Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + "
    Enfocar la barra d'eines
    \n" + + '
    Windows o Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Enfocar el peu de pàgina
    \n' + + '
    Windows o Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Enfocar la notificació
    \n' + + '
    Windows o Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + "
    Enfocar una barra d'eines contextual
    \n" + + '
    Windows, Linux o macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + "

    La navegació començarà en el primer element de la interfície d'usuari, que es ressaltarà o subratllarà per al primer element a\n" + + " la ruta de l'element de peu de pàgina.

    \n" + + '\n' + + "

    Navegació entre seccions de la interfície d'usuari

    \n" + + '\n' + + "

    Per desplaçar-vos des d'una secció de la interfície d'usuari a la següent, premeu la tecla Tab.

    \n" + + '\n' + + "

    Per desplaçar-vos des d'una secció de la interfície d'usuari a l'anterior, premeu les tecles Maj+Tab.

    \n" + + '\n' + + "

    L'ordre en prémer la tecla Tab d'aquestes secciones de la interfície d'usuari és:

    \n" + + '\n' + + '
      \n' + + '
    1. Barra de menús
    2. \n' + + "
    3. Cada grup de la barra d'eines
    4. \n" + + '
    5. Barra lateral
    6. \n' + + "
    7. Ruta de l'element del peu de pàgina
    8. \n" + + '
    9. Botó de commutació de recompte de paraules al peu de pàgina
    10. \n' + + '
    11. Enllaç de marca del peu de pàgina
    12. \n' + + "
    13. Control de canvi de mida de l'editor al peu de pàgina
    14. \n" + + '
    \n' + + '\n' + + "

    Si no hi ha una secció de la interfície d'usuari, s'ometrà.

    \n" + + '\n' + + '

    Si el peu de pàgina té el focus de navegació del teclat i no hi ha cap barra lateral visible, en prémer Maj+Tab\n' + + " el focus es mou al primer grup de la barra d'eines, no l'últim.

    \n" + + '\n' + + "

    Navegació dins de les seccions de la interfície d'usuari

    \n" + + '\n' + + "

    Per desplaçar-vos des d'un element de la interfície d'usuari al següent, premeu la tecla de Fletxa adequada.

    \n" + + '\n' + + '

    Les tecles de fletxa Esquerra i Dreta

    \n' + + '\n' + + '
      \n' + + '
    • us permeten desplaçar-vos entre menús de la barra de menús.
    • \n' + + '
    • obren un submenú en un menú.
    • \n' + + "
    • us permeten desplaçar-vos entre botons d'un grup de la barra d'eines.
    • \n" + + "
    • us permeten desplaçar-vos entre elements de la ruta d'elements del peu de pàgina.
    • \n" + + '
    \n' + + '\n' + + '

    Les tecles de fletxa Avall i Amunt

    \n' + + '\n' + + '
      \n' + + "
    • us permeten desplaçar-vos entre elements de menú d'un menú.
    • \n" + + "
    • us permeten desplaçar-vos entre elements d'un menú emergent de la barra d'eines.
    • \n" + + '
    \n' + + '\n' + + "

    Les tecles de Fletxa us permeten desplaçar-vos dins de la secció de la interfície d'usuari que té el focus.

    \n" + + '\n' + + '

    Per tancar un menú, un submenú o un menú emergent oberts, premeu la tecla Esc.

    \n' + + '\n' + + "

    Si el focus actual es troba a la ‘part superior’ d'una secció específica de la interfície d'usuari, en prémer la tecla Esc també es tanca\n" + + ' completament la navegació amb el teclat.

    \n' + + '\n' + + "

    Execució d'un element de menú o d'un botó de la barra d'eines

    \n" + + '\n' + + "

    Quan l'element del menú o el botó de la barra d'eines que desitgeu estigui ressaltat, premeu Retorn, Intro\n" + + " o la barra d'espai per executar l'element.

    \n" + + '\n' + + '

    Navegació per quadres de diàleg sense pestanyes

    \n' + + '\n' + + "

    En els quadres de diàleg sense pestanyes, el primer component interactiu pren el focus quan s'obre el quadre diàleg.

    \n" + + '\n' + + '

    Premeu la tecla Tab o les tecles Maj+Tab per desplaçar-vos entre components interactius del quadre de diàleg.

    \n' + + '\n' + + '

    Navegació per quadres de diàleg amb pestanyes

    \n' + + '\n' + + "

    En els quadres de diàleg amb pestanyes, el primer botó del menú de la pestanya pren el focus quan s'obre el quadre diàleg.

    \n" + + '\n' + + "

    Per desplaçar-vos entre components interactius d'aquest quadre de diàleg, premeu la tecla Tab o\n" + + ' les tecles Maj+Tab.

    \n' + + '\n' + + "

    Canvieu a la pestanya d'un altre quadre de diàleg, tot enfocant el menú de la pestanya, i després premeu la tecla Fletxa adequada\n" + + ' per canviar entre les pestanyes disponibles.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/cs.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/cs.js new file mode 100644 index 0000000..4a5a902 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/cs.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.cs', +'

    Začínáme navigovat pomocí klávesnice

    \n' + + '\n' + + '
    \n' + + '
    Přejít na řádek nabídek
    \n' + + '
    Windows nebo Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Přejít na panel nástrojů
    \n' + + '
    Windows nebo Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Přejít na zápatí
    \n' + + '
    Windows nebo Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Přejít na oznámení
    \n' + + '
    Windows nebo Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Přejít na kontextový panel nástrojů
    \n' + + '
    Windows, Linux nebo macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigace začne u první položky uživatelského rozhraní, která bude zvýrazněna nebo v případě první položky\n' + + ' cesty k prvku zápatí podtržena.

    \n' + + '\n' + + '

    Navigace mezi oddíly uživatelského rozhraní

    \n' + + '\n' + + '

    Stisknutím klávesy Tab se posunete z jednoho oddílu uživatelského rozhraní na další.

    \n' + + '\n' + + '

    Stisknutím kláves Shift+Tab se posunete z jednoho oddílu uživatelského rozhraní na předchozí.

    \n' + + '\n' + + '

    Pořadí přepínání mezi oddíly uživatelského rozhraní pomocí klávesy Tab:

    \n' + + '\n' + + '
      \n' + + '
    1. Řádek nabídek
    2. \n' + + '
    3. Každá skupina panelu nástrojů
    4. \n' + + '
    5. Boční panel
    6. \n' + + '
    7. Cesta k prvku v zápatí.
    8. \n' + + '
    9. Tlačítko přepínače počtu slov v zápatí
    10. \n' + + '
    11. Odkaz na informace o značce v zápatí
    12. \n' + + '
    13. Úchyt pro změnu velikosti editoru v zápatí
    14. \n' + + '
    \n' + + '\n' + + '

    Pokud nějaký oddíl uživatelského rozhraní není přítomen, je přeskočen.

    \n' + + '\n' + + '

    Pokud je zápatí vybrané pro navigaci pomocí klávesnice a není zobrazen žádný boční panel, stisknutím kláves Shift+Tab\n' + + ' přejdete na první skupinu panelu nástrojů, nikoli na poslední.

    \n' + + '\n' + + '

    Navigace v rámci oddílů uživatelského rozhraní

    \n' + + '\n' + + '

    Chcete-li se přesunout z jednoho prvku uživatelského rozhraní na další, stiskněte příslušnou klávesu s šipkou.

    \n' + + '\n' + + '

    Klávesy s šipkou vlevovpravo

    \n' + + '\n' + + '
      \n' + + '
    • umožňují přesun mezi nabídkami na řádku nabídek;
    • \n' + + '
    • otevírají podnabídku nabídky;
    • \n' + + '
    • umožňují přesun mezi tlačítky ve skupině panelu nástrojů;
    • \n' + + '
    • umožňují přesun mezi položkami cesty prvku v zápatí.
    • \n' + + '
    \n' + + '\n' + + '

    Klávesy se šipkou dolůnahoru

    \n' + + '\n' + + '
      \n' + + '
    • umožňují přesun mezi položkami nabídky;
    • \n' + + '
    • umožňují přesun mezi položkami místní nabídky panelu nástrojů.
    • \n' + + '
    \n' + + '\n' + + '

    Šipky provádí přepínání v rámci vybraného oddílu uživatelského rozhraní.

    \n' + + '\n' + + '

    Chcete-li zavřít otevřenou nabídku, podnabídku nebo místní nabídku, stiskněte klávesu Esc.

    \n' + + '\n' + + '

    Pokud je aktuálně vybrána horní část oddílu uživatelského rozhraní, stisknutím klávesy Esc zcela ukončíte také\n' + + ' navigaci pomocí klávesnice.

    \n' + + '\n' + + '

    Provedení příkazu položky nabídky nebo tlačítka panelu nástrojů

    \n' + + '\n' + + '

    Pokud je zvýrazněna požadovaná položka nabídky nebo tlačítko panelu nástrojů, stisknutím klávesy Return, Enter\n' + + ' nebo mezerníku provedete příslušný příkaz.

    \n' + + '\n' + + '

    Navigace v dialogových oknech bez záložek

    \n' + + '\n' + + '

    Při otevření dialogových oken bez záložek přejdete na první interaktivní komponentu.

    \n' + + '\n' + + '

    Přecházet mezi interaktivními komponentami dialogového okna můžete stisknutím klávesy Tab nebo kombinace Shift+Tab.

    \n' + + '\n' + + '

    Navigace v dialogových oknech se záložkami

    \n' + + '\n' + + '

    Při otevření dialogových oken se záložkami přejdete na první tlačítko v nabídce záložek.

    \n' + + '\n' + + '

    Přecházet mezi interaktivními komponentami této záložky dialogového okna můžete stisknutím klávesy Tab nebo\n' + + ' kombinace Shift+Tab.

    \n' + + '\n' + + '

    Chcete-li přepnout na další záložku dialogového okna, přejděte na nabídku záložek a poté můžete stisknutím požadované šipky\n' + + ' přepínat mezi dostupnými záložkami.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/da.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/da.js new file mode 100644 index 0000000..4d1e1d4 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/da.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.da', +'

    Start tastaturnavigation

    \n' + + '\n' + + '
    \n' + + '
    Fokuser på menulinjen
    \n' + + '
    Windows eller Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokuser på værktøjslinjen
    \n' + + '
    Windows eller Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokuser på sidefoden
    \n' + + '
    Windows eller Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Fokuser på meddelelsen
    \n' + + '
    Windows eller Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokuser på kontekstuel værktøjslinje
    \n' + + '
    Windows, Linux eller macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigationen starter ved det første UI-element, som fremhæves eller understreges hvad angår det første element i\n' + + ' sidefodens sti til elementet.

    \n' + + '\n' + + '

    Naviger mellem UI-sektioner

    \n' + + '\n' + + '

    Gå fra én UI-sektion til den næste ved at trykke på Tab.

    \n' + + '\n' + + '

    Gå fra én UI-sektion til den forrige ved at trykke på Shift+Tab.

    \n' + + '\n' + + '

    Tab-rækkefølgen af disse UI-sektioner er:

    \n' + + '\n' + + '
      \n' + + '
    1. Menulinje
    2. \n' + + '
    3. Hver værktøjsgruppe
    4. \n' + + '
    5. Sidepanel
    6. \n' + + '
    7. Sti til elementet i sidefoden
    8. \n' + + '
    9. Til/fra-knap for ordoptælling i sidefoden
    10. \n' + + '
    11. Brandinglink i sidefoden
    12. \n' + + '
    13. Tilpasningshåndtag for editor i sidefoden
    14. \n' + + '
    \n' + + '\n' + + '

    Hvis en UI-sektion ikke er til stede, springes den over.

    \n' + + '\n' + + '

    Hvis sidefoden har fokus til tastaturnavigation, og der ikke er noget synligt sidepanel, kan der trykkes på Shift+Tab\n' + + ' for at flytte fokus til den første værktøjsgruppe, ikke den sidste.

    \n' + + '\n' + + '

    Naviger inden for UI-sektioner

    \n' + + '\n' + + '

    Gå fra ét UI-element til det næste ved at trykke på den relevante piletast.

    \n' + + '\n' + + '

    Venstre og højre piletast

    \n' + + '\n' + + '
      \n' + + '
    • flytter mellem menuerne i menulinjen.
    • \n' + + '
    • åbner en undermenu i en menu.
    • \n' + + '
    • flytter mellem knapperne i en værktøjsgruppe.
    • \n' + + '
    • flytter mellem elementer i sidefodens sti til elementet.
    • \n' + + '
    \n' + + '\n' + + '

    Pil ned og op

    \n' + + '\n' + + '
      \n' + + '
    • flytter mellem menupunkterne i en menu.
    • \n' + + '
    • flytter mellem punkterne i en genvejsmenu i værktøjslinjen.
    • \n' + + '
    \n' + + '\n' + + '

    Piletasterne kører rundt inden for UI-sektionen, der fokuseres på.

    \n' + + '\n' + + '

    For at lukke en åben menu, en åben undermenu eller en åben genvejsmenu trykkes der på Esc-tasten.

    \n' + + '\n' + + "

    Hvis det aktuelle fokus er i 'toppen' af en bestemt UI-sektion, vil tryk på Esc-tasten også afslutte\n" + + ' tastaturnavigationen helt.

    \n' + + '\n' + + '

    Udfør et menupunkt eller en værktøjslinjeknap

    \n' + + '\n' + + '

    Når det ønskede menupunkt eller den ønskede værktøjslinjeknap er fremhævet, trykkes der på Retur, Enter\n' + + ' eller mellemrumstasten for at udføre elementet.

    \n' + + '\n' + + '

    Naviger i ikke-faneopdelte dialogbokse

    \n' + + '\n' + + '

    I ikke-faneopdelte dialogbokse får den første interaktive komponent fokus, når dialogboksen åbnes.

    \n' + + '\n' + + '

    Naviger mellem interaktive dialogbokskomponenter ved at trykke på Tab eller Shift+Tab.

    \n' + + '\n' + + '

    Naviger i faneopdelte dialogbokse

    \n' + + '\n' + + '

    I faneopdelte dialogbokse får den første knap i fanemenuen fokus, når dialogboksen åbnes.

    \n' + + '\n' + + '

    Naviger mellem interaktive komponenter i denne dialogboksfane ved at trykke på Tab eller\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Skift til en anden dialogboksfane ved at fokusere på fanemenuen og derefter trykke på den relevante piletast\n' + + ' for at køre igennem de tilgængelige faner.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/de.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/de.js new file mode 100644 index 0000000..b8711ed --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/de.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.de', +'

    Grundlagen der Tastaturnavigation

    \n' + + '\n' + + '
    \n' + + '
    Fokus auf Menüleiste
    \n' + + '
    Windows oder Linux: ALT+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokus auf Symbolleiste
    \n' + + '
    Windows oder Linux: ALT+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokus auf Fußzeile
    \n' + + '
    Windows oder Linux: ALT+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Benachrichtigung fokussieren
    \n' + + '
    Windows oder Linux: ALT+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokus auf kontextbezogene Symbolleiste
    \n' + + '
    Windows, Linux oder macOS: STRG+F9
    \n' + + '
    \n' + + '\n' + + '

    Die Navigation beginnt beim ersten Benutzeroberflächenelement, welches hervorgehoben ist. Falls sich das erste Element im Pfad der Fußzeile befindet,\n' + + ' ist es unterstrichen.

    \n' + + '\n' + + '

    Zwischen Abschnitten der Benutzeroberfläche navigieren

    \n' + + '\n' + + '

    Um von einem Abschnitt der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie TAB.

    \n' + + '\n' + + '

    Um von einem Abschnitt der Benutzeroberfläche zum vorherigen zu wechseln, drücken Sie UMSCHALT+TAB.

    \n' + + '\n' + + '

    Die Abschnitte der Benutzeroberfläche haben folgende TAB-Reihenfolge:

    \n' + + '\n' + + '
      \n' + + '
    1. Menüleiste
    2. \n' + + '
    3. Einzelne Gruppen der Symbolleiste
    4. \n' + + '
    5. Randleiste
    6. \n' + + '
    7. Elementpfad in der Fußzeile
    8. \n' + + '
    9. Umschaltfläche „Wörter zählen“ in der Fußzeile
    10. \n' + + '
    11. Branding-Link in der Fußzeile
    12. \n' + + '
    13. Editor-Ziehpunkt zur Größenänderung in der Fußzeile
    14. \n' + + '
    \n' + + '\n' + + '

    Falls ein Abschnitt der Benutzeroberflächen nicht vorhanden ist, wird er übersprungen.

    \n' + + '\n' + + '

    Wenn in der Fußzeile die Tastaturnavigation fokussiert ist und keine Randleiste angezeigt wird, wechselt der Fokus durch Drücken von UMSCHALT+TAB\n' + + ' zur ersten Gruppe der Symbolleiste, nicht zur letzten.

    \n' + + '\n' + + '

    Innerhalb von Abschnitten der Benutzeroberfläche navigieren

    \n' + + '\n' + + '

    Um von einem Element der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie die entsprechende Pfeiltaste.

    \n' + + '\n' + + '

    Die Pfeiltasten Links und Rechts

    \n' + + '\n' + + '
      \n' + + '
    • wechseln zwischen Menüs in der Menüleiste.
    • \n' + + '
    • öffnen das Untermenü eines Menüs.
    • \n' + + '
    • wechseln zwischen Schaltflächen in einer Gruppe der Symbolleiste.
    • \n' + + '
    • wechseln zwischen Elementen im Elementpfad der Fußzeile.
    • \n' + + '
    \n' + + '\n' + + '

    Die Pfeiltasten Abwärts und Aufwärts

    \n' + + '\n' + + '
      \n' + + '
    • wechseln zwischen Menüelementen in einem Menü.
    • \n' + + '
    • wechseln zwischen Elementen in einem Popupmenü der Symbolleiste.
    • \n' + + '
    \n' + + '\n' + + '

    Die Pfeiltasten rotieren innerhalb des fokussierten Abschnitts der Benutzeroberfläche.

    \n' + + '\n' + + '

    Um ein geöffnetes Menü, ein geöffnetes Untermenü oder ein geöffnetes Popupmenü zu schließen, drücken Sie die ESC-Taste.

    \n' + + '\n' + + '

    Wenn sich der aktuelle Fokus ganz oben in einem bestimmten Abschnitt der Benutzeroberfläche befindet, wird durch Drücken der ESC-Taste auch\n' + + ' die Tastaturnavigation beendet.

    \n' + + '\n' + + '

    Ein Menüelement oder eine Symbolleistenschaltfläche ausführen

    \n' + + '\n' + + '

    Wenn das gewünschte Menüelement oder die gewünschte Symbolleistenschaltfläche hervorgehoben ist, drücken Sie Zurück, Eingabe\n' + + ' oder die Leertaste, um das Element auszuführen.

    \n' + + '\n' + + '

    In Dialogfeldern ohne Registerkarten navigieren

    \n' + + '\n' + + '

    In Dialogfeldern ohne Registerkarten ist beim Öffnen eines Dialogfelds die erste interaktive Komponente fokussiert.

    \n' + + '\n' + + '

    Navigieren Sie zwischen den interaktiven Komponenten eines Dialogfelds, indem Sie TAB oder UMSCHALT+TAB drücken.

    \n' + + '\n' + + '

    In Dialogfeldern mit Registerkarten navigieren

    \n' + + '\n' + + '

    In Dialogfeldern mit Registerkarten ist beim Öffnen eines Dialogfelds die erste Schaltfläche eines Registerkartenmenüs fokussiert.

    \n' + + '\n' + + '

    Navigieren Sie zwischen den interaktiven Komponenten auf dieser Registerkarte des Dialogfelds, indem Sie TAB oder\n' + + ' UMSCHALT+TAB drücken.

    \n' + + '\n' + + '

    Wechseln Sie zu einer anderen Registerkarte des Dialogfelds, indem Sie den Fokus auf das Registerkartenmenü legen und dann die entsprechende Pfeiltaste\n' + + ' drücken, um durch die verfügbaren Registerkarten zu rotieren.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/el.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/el.js new file mode 100644 index 0000000..98afabe --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/el.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.el', +'

    Έναρξη πλοήγησης μέσω πληκτρολογίου

    \n' + + '\n' + + '
    \n' + + '
    Εστίαση στη γραμμή μενού
    \n' + + '
    Windows ή Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Εστίαση στη γραμμή εργαλείων
    \n' + + '
    Windows ή Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Εστίαση στο υποσέλιδο
    \n' + + '
    Windows ή Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Εστίαση στην ειδοποίηση
    \n' + + '
    Windows ή Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Εστίαση σε γραμμή εργαλείων βάσει περιεχομένου
    \n' + + '
    Windows, Linux ή macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Η πλοήγηση θα ξεκινήσει από το πρώτο στοιχείο περιβάλλοντος χρήστη, που θα επισημαίνεται ή θα είναι υπογραμμισμένο,\n' + + ' όπως στην περίπτωση της διαδρομής του στοιχείου Υποσέλιδου.

    \n' + + '\n' + + '

    Πλοήγηση μεταξύ ενοτήτων του περιβάλλοντος χρήστη

    \n' + + '\n' + + '

    Για να μετακινηθείτε από μια ενότητα περιβάλλοντος χρήστη στην επόμενη, πιέστε το πλήκτρο Tab.

    \n' + + '\n' + + '

    Για να μετακινηθείτε από μια ενότητα περιβάλλοντος χρήστη στην προηγούμενη, πιέστε τα πλήκτρα Shift+Tab.

    \n' + + '\n' + + '

    Η σειρά Tab αυτών των ενοτήτων περιβάλλοντος χρήστη είναι η εξής:

    \n' + + '\n' + + '
      \n' + + '
    1. Γραμμή μενού
    2. \n' + + '
    3. Κάθε ομάδα γραμμής εργαλείων
    4. \n' + + '
    5. Πλαϊνή γραμμή
    6. \n' + + '
    7. Διαδρομή στοιχείου στο υποσέλιδο
    8. \n' + + '
    9. Κουμπί εναλλαγής μέτρησης λέξεων στο υποσέλιδο
    10. \n' + + '
    11. Σύνδεσμος επωνυμίας στο υποσέλιδο
    12. \n' + + '
    13. Λαβή αλλαγής μεγέθους προγράμματος επεξεργασίας στο υποσέλιδο
    14. \n' + + '
    \n' + + '\n' + + '

    Εάν δεν εμφανίζεται ενότητα περιβάλλοντος χρήστη, παραλείπεται.

    \n' + + '\n' + + '

    Εάν η εστίαση πλοήγησης βρίσκεται στο πληκτρολόγιο και δεν υπάρχει εμφανής πλαϊνή γραμμή, εάν πιέσετε Shift+Tab\n' + + ' η εστίαση μετακινείται στην πρώτη ομάδα γραμμής εργαλείων, όχι στην τελευταία.

    \n' + + '\n' + + '

    Πλοήγηση εντός των ενοτήτων του περιβάλλοντος χρήστη

    \n' + + '\n' + + '

    Για να μετακινηθείτε από ένα στοιχείο περιβάλλοντος χρήστη στο επόμενο, πιέστε το αντίστοιχο πλήκτρο βέλους.

    \n' + + '\n' + + '

    Με τα πλήκτρα αριστερού και δεξιού βέλους

    \n' + + '\n' + + '
      \n' + + '
    • γίνεται μετακίνηση μεταξύ των μενού στη γραμμή μενού.
    • \n' + + '
    • ανοίγει ένα υπομενού σε ένα μενού.
    • \n' + + '
    • γίνεται μετακίνηση μεταξύ κουμπιών σε μια ομάδα γραμμής εργαλείων.
    • \n' + + '
    • γίνεται μετακίνηση μεταξύ στοιχείων στη διαδρομή στοιχείου στο υποσέλιδο.
    • \n' + + '
    \n' + + '\n' + + '

    Με τα πλήκτρα επάνω και κάτω βέλους

    \n' + + '\n' + + '
      \n' + + '
    • γίνεται μετακίνηση μεταξύ των στοιχείων μενού σε ένα μενού.
    • \n' + + '
    • γίνεται μετακίνηση μεταξύ των στοιχείων μενού σε ένα αναδυόμενο μενού γραμμής εργαλείων.
    • \n' + + '
    \n' + + '\n' + + '

    Με τα πλήκτρα βέλους γίνεται κυκλική μετακίνηση εντός της εστιασμένης ενότητας περιβάλλοντος χρήστη.

    \n' + + '\n' + + '

    Για να κλείσετε ένα ανοιχτό μενού, ένα ανοιχτό υπομενού ή ένα ανοιχτό αναδυόμενο μενού, πιέστε το πλήκτρο Esc.

    \n' + + '\n' + + '

    Εάν η τρέχουσα εστίαση βρίσκεται στην κορυφή μιας ενότητας περιβάλλοντος χρήστη, πιέζοντας το πλήκτρο Esc,\n' + + ' γίνεται επίσης πλήρης έξοδος από την πλοήγηση μέσω πληκτρολογίου.

    \n' + + '\n' + + '

    Εκτέλεση ενός στοιχείου μενού ή κουμπιού γραμμής εργαλείων

    \n' + + '\n' + + '

    Όταν το επιθυμητό στοιχείο μενού ή κουμπί γραμμής εργαλείων είναι επισημασμένο, πιέστε τα πλήκτρα Return, Enter,\n' + + ' ή το πλήκτρο διαστήματος για να εκτελέσετε το στοιχείο.

    \n' + + '\n' + + '

    Πλοήγηση σε παράθυρα διαλόγου χωρίς καρτέλες

    \n' + + '\n' + + '

    Σε παράθυρα διαλόγου χωρίς καρτέλες, το πρώτο αλληλεπιδραστικό στοιχείο λαμβάνει την εστίαση όταν ανοίγει το παράθυρο διαλόγου.

    \n' + + '\n' + + '

    Μπορείτε να πλοηγηθείτε μεταξύ των αλληλεπιδραστικών στοιχείων παραθύρων διαλόγων πιέζοντας τα πλήκτρα Tab ή Shift+Tab.

    \n' + + '\n' + + '

    Πλοήγηση σε παράθυρα διαλόγου με καρτέλες

    \n' + + '\n' + + '

    Σε παράθυρα διαλόγου με καρτέλες, το πρώτο κουμπί στο μενού καρτέλας λαμβάνει την εστίαση όταν ανοίγει το παράθυρο διαλόγου.

    \n' + + '\n' + + '

    Μπορείτε να πλοηγηθείτε μεταξύ των αλληλεπιδραστικών στοιχείων αυτής της καρτέλα διαλόγου πιέζοντας τα πλήκτρα Tab ή\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Μπορείτε να κάνετε εναλλαγή σε άλλη καρτέλα του παραθύρου διαλόγου, μεταφέροντας την εστίαση στο μενού καρτέλας και πιέζοντας το κατάλληλο πλήκτρο βέλους\n' + + ' για να μετακινηθείτε κυκλικά στις διαθέσιμες καρτέλες.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/en.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/en.js new file mode 100644 index 0000000..5dd753e --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/en.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.en', +'

    Begin keyboard navigation

    \n' + + '\n' + + '
    \n' + + '
    Focus the Menu bar
    \n' + + '
    Windows or Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Focus the Toolbar
    \n' + + '
    Windows or Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Focus the footer
    \n' + + '
    Windows or Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Focus the notification
    \n' + + '
    Windows or Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Focus a contextual toolbar
    \n' + + '
    Windows, Linux or macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigation will start at the first UI item, which will be highlighted, or underlined in the case of the first item in\n' + + ' the Footer element path.

    \n' + + '\n' + + '

    Navigate between UI sections

    \n' + + '\n' + + '

    To move from one UI section to the next, press Tab.

    \n' + + '\n' + + '

    To move from one UI section to the previous, press Shift+Tab.

    \n' + + '\n' + + '

    The Tab order of these UI sections is:

    \n' + + '\n' + + '
      \n' + + '
    1. Menu bar
    2. \n' + + '
    3. Each toolbar group
    4. \n' + + '
    5. Sidebar
    6. \n' + + '
    7. Element path in the footer
    8. \n' + + '
    9. Word count toggle button in the footer
    10. \n' + + '
    11. Branding link in the footer
    12. \n' + + '
    13. Editor resize handle in the footer
    14. \n' + + '
    \n' + + '\n' + + '

    If a UI section is not present, it is skipped.

    \n' + + '\n' + + '

    If the footer has keyboard navigation focus, and there is no visible sidebar, pressing Shift+Tab\n' + + ' moves focus to the first toolbar group, not the last.

    \n' + + '\n' + + '

    Navigate within UI sections

    \n' + + '\n' + + '

    To move from one UI element to the next, press the appropriate Arrow key.

    \n' + + '\n' + + '

    The Left and Right arrow keys

    \n' + + '\n' + + '
      \n' + + '
    • move between menus in the menu bar.
    • \n' + + '
    • open a sub-menu in a menu.
    • \n' + + '
    • move between buttons in a toolbar group.
    • \n' + + '
    • move between items in the footer’s element path.
    • \n' + + '
    \n' + + '\n' + + '

    The Down and Up arrow keys

    \n' + + '\n' + + '
      \n' + + '
    • move between menu items in a menu.
    • \n' + + '
    • move between items in a toolbar pop-up menu.
    • \n' + + '
    \n' + + '\n' + + '

    Arrow keys cycle within the focused UI section.

    \n' + + '\n' + + '

    To close an open menu, an open sub-menu, or an open pop-up menu, press the Esc key.

    \n' + + '\n' + + '

    If the current focus is at the ‘top’ of a particular UI section, pressing the Esc key also exits\n' + + ' keyboard navigation entirely.

    \n' + + '\n' + + '

    Execute a menu item or toolbar button

    \n' + + '\n' + + '

    When the desired menu item or toolbar button is highlighted, press Return, Enter,\n' + + ' or the Space bar to execute the item.

    \n' + + '\n' + + '

    Navigate non-tabbed dialogs

    \n' + + '\n' + + '

    In non-tabbed dialogs, the first interactive component takes focus when the dialog opens.

    \n' + + '\n' + + '

    Navigate between interactive dialog components by pressing Tab or Shift+Tab.

    \n' + + '\n' + + '

    Navigate tabbed dialogs

    \n' + + '\n' + + '

    In tabbed dialogs, the first button in the tab menu takes focus when the dialog opens.

    \n' + + '\n' + + '

    Navigate between interactive components of this dialog tab by pressing Tab or\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Switch to another dialog tab by giving the tab menu focus and then pressing the appropriate Arrow\n' + + ' key to cycle through the available tabs.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/es.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/es.js new file mode 100644 index 0000000..e426c2e --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/es.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.es', +'

    Iniciar la navegación con el teclado

    \n' + + '\n' + + '
    \n' + + '
    Enfocar la barra de menús
    \n' + + '
    Windows o Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Enfocar la barra de herramientas
    \n' + + '
    Windows o Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Enfocar el pie de página
    \n' + + '
    Windows o Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Enfocar la notificación
    \n' + + '
    Windows o Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Enfocar una barra de herramientas contextual
    \n' + + '
    Windows, Linux o macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    La navegación comenzará por el primer elemento de la interfaz de usuario (IU), de tal manera que se resaltará, o bien se subrayará si se trata del primer elemento de\n' + + ' la ruta de elemento del pie de página.

    \n' + + '\n' + + '

    Navegar entre las secciones de la IU

    \n' + + '\n' + + '

    Para pasar de una sección de la IU a la siguiente, pulse la tecla Tab.

    \n' + + '\n' + + '

    Para pasar de una sección de la IU a la anterior, pulse Mayús+Tab.

    \n' + + '\n' + + '

    El orden de tabulación de estas secciones de la IU es:

    \n' + + '\n' + + '
      \n' + + '
    1. Barra de menús
    2. \n' + + '
    3. Cada grupo de barra de herramientas
    4. \n' + + '
    5. Barra lateral
    6. \n' + + '
    7. Ruta del elemento en el pie de página
    8. \n' + + '
    9. Botón de alternancia de recuento de palabras en el pie de página
    10. \n' + + '
    11. Enlace de personalización de marca en el pie de página
    12. \n' + + '
    13. Controlador de cambio de tamaño en el pie de página
    14. \n' + + '
    \n' + + '\n' + + '

    Si una sección de la IU no está presente, esta se omite.

    \n' + + '\n' + + '

    Si el pie de página tiene un enfoque de navegación con el teclado y no hay ninguna barra lateral visible, al pulsar Mayús+Tab,\n' + + ' el enfoque se moverá al primer grupo de barra de herramientas, en lugar de al último.

    \n' + + '\n' + + '

    Navegar dentro de las secciones de la IU

    \n' + + '\n' + + '

    Para pasar de un elemento de la IU al siguiente, pulse la tecla de flecha correspondiente.

    \n' + + '\n' + + '

    Las teclas de flecha izquierda y derecha permiten

    \n' + + '\n' + + '
      \n' + + '
    • desplazarse entre los menús de la barra de menús.
    • \n' + + '
    • abrir el submenú de un menú.
    • \n' + + '
    • desplazarse entre los botones de un grupo de barra de herramientas.
    • \n' + + '
    • desplazarse entre los elementos de la ruta de elemento del pie de página.
    • \n' + + '
    \n' + + '\n' + + '

    Las teclas de flecha abajo y arriba permiten

    \n' + + '\n' + + '
      \n' + + '
    • desplazarse entre los elementos de menú de un menú.
    • \n' + + '
    • desplazarse entre los elementos de un menú emergente de una barra de herramientas.
    • \n' + + '
    \n' + + '\n' + + '

    Las teclas de flecha van cambiando dentro de la sección de la IU enfocada.

    \n' + + '\n' + + '

    Para cerrar un menú, un submenú o un menú emergente que estén abiertos, pulse la tecla Esc.

    \n' + + '\n' + + '

    Si el enfoque actual se encuentra en la parte superior de una sección de la IU determinada, al pulsar la tecla Esc saldrá\n' + + ' de la navegación con el teclado por completo.

    \n' + + '\n' + + '

    Ejecutar un elemento de menú o un botón de barra de herramientas

    \n' + + '\n' + + '

    Si el elemento de menú o el botón de barra de herramientas deseado está resaltado, pulse la tecla Retorno o Entrar,\n' + + ' o la barra espaciadora para ejecutar el elemento.

    \n' + + '\n' + + '

    Navegar por cuadros de diálogo sin pestañas

    \n' + + '\n' + + '

    En los cuadros de diálogo sin pestañas, el primer componente interactivo se enfoca al abrirse el cuadro de diálogo.

    \n' + + '\n' + + '

    Para navegar entre los componentes interactivos del cuadro de diálogo, pulse las teclas Tab o Mayús+Tab.

    \n' + + '\n' + + '

    Navegar por cuadros de diálogo con pestañas

    \n' + + '\n' + + '

    En los cuadros de diálogo con pestañas, el primer botón del menú de pestaña se enfoca al abrirse el cuadro de diálogo.

    \n' + + '\n' + + '

    Para navegar entre componentes interactivos de esta pestaña del cuadro de diálogo, pulse las teclas Tab o\n' + + ' Mayús+Tab.

    \n' + + '\n' + + '

    Si desea cambiar a otra pestaña del cuadro de diálogo, enfoque el menú de pestañas y, a continuación, pulse la tecla de flecha\n' + + ' correspondiente para moverse por las pestañas disponibles.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/eu.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/eu.js new file mode 100644 index 0000000..c18b940 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/eu.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.eu', +'

    Hasi teklatuaren nabigazioa

    \n' + + '\n' + + '
    \n' + + '
    Fokuratu menu-barra
    \n' + + '
    Windows edo Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokuratu tresna-barra
    \n' + + '
    Windows edo Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokuratu orri-oina
    \n' + + '
    Windows edo Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Fokuratu jakinarazpena
    \n' + + '
    Windows edo Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokuratu testuinguruaren tresna-barra
    \n' + + '
    Windows, Linux edo macOS: Ktrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Nabigazioa EIko lehen elementuan hasiko da: elementu hori nabarmendu egingo da, edo azpimarratu lehen elementua bada\n' + + ' orri-oineko elementuaren bidea.

    \n' + + '\n' + + '

    Nabigatu EIko atalen artean

    \n' + + '\n' + + '

    EIko atal batetik hurrengora mugitzeko, sakatu Tabuladorea.

    \n' + + '\n' + + '

    EIko atal batetik aurrekora mugitzeko, sakatu Maius+Tabuladorea.

    \n' + + '\n' + + '

    EIko atal hauen Tabuladorea da:

    \n' + + '\n' + + '
      \n' + + '
    1. Menu-barra
    2. \n' + + '
    3. Tresna-barraren talde bakoitza
    4. \n' + + '
    5. Alboko barra
    6. \n' + + '
    7. Orri-oineko elementuaren bidea
    8. \n' + + '
    9. Orri-oneko urrats-kontaketa txandakatzeko botoia
    10. \n' + + '
    11. Orri-oineko marken esteka
    12. \n' + + '
    13. Orri-oineko editorearen tamaina aldatzeko heldulekua
    14. \n' + + '
    \n' + + '\n' + + '

    EIko atal bat ez badago, saltatu egin da.

    \n' + + '\n' + + '

    Orri-oinak teklatuaren nabigazioa fokuratuta badago, eta alboko barra ikusgai ez badago, Maius+Tabuladorea sakatuz gero,\n' + + ' fokua tresna-barrako lehen taldera eramaten da, ez azkenera.

    \n' + + '\n' + + '

    Nabigatu EIko atalen barruan

    \n' + + '\n' + + '

    EIko elementu batetik hurrengora mugitzeko, sakatu dagokion Gezia tekla.

    \n' + + '\n' + + '

    Ezkerrera eta Eskuinera gezi-teklak

    \n' + + '\n' + + '
      \n' + + '
    • menu-barrako menuen artean mugitzen da.
    • \n' + + '
    • ireki azpimenu bat menuan.
    • \n' + + '
    • mugitu botoi batetik bestera tresna-barren talde batean.
    • \n' + + '
    • mugitu orri-oineko elementuaren bideko elementu batetik bestera.
    • \n' + + '
    \n' + + '\n' + + '

    Gora eta Behera gezi-teklak

    \n' + + '\n' + + '
      \n' + + '
    • mugitu menu bateko menu-elementuen artean.
    • \n' + + '
    • mugitu tresna-barrako menu gainerakor bateko menu-elementuen artean.
    • \n' + + '
    \n' + + '\n' + + '

    Gezia teklen zikloa nabarmendutako EI atalen barruan.

    \n' + + '\n' + + '

    Irekitako menu bat ixteko, ireki azpimenua, edo ireki menu gainerakorra, sakatu Ihes tekla.

    \n' + + '\n' + + '

    Une horretan fokuratzea EIko atal jakin baten "goialdean" badago, Ihes tekla sakatuz gero\n' + + ' teklatuaren nabigaziotik irtengo zara.

    \n' + + '\n' + + '

    Exekutatu menuko elementu bat edo tresna-barrako botoi bat

    \n' + + '\n' + + '

    Nahi den menuaren elementua edo tresna-barraren botoia nabarmenduta dagoenean, sakatu Itzuli, Sartu\n' + + ' edo Zuriune-barra elementua exekutatzeko.

    \n' + + '\n' + + '

    Nabigatu fitxarik gabeko elkarrizketak

    \n' + + '\n' + + '

    Fitxarik gabeko elkarrizketetan, lehen osagai interaktiboa fokuratzen da elkarrizketa irekitzen denean.

    \n' + + '\n' + + '

    Nabigatu elkarrizketa interaktiboko osagai batetik bestera Tabuladorea edo Maius+Tabuladorea sakatuta.

    \n' + + '\n' + + '

    Nabigatu fitxadun elkarrizketak

    \n' + + '\n' + + '

    Fitxadun elkarrizketetan, fitxa-menuko lehen botoia fokuratzen da elkarrizketa irekitzen denean.

    \n' + + '\n' + + '

    Nabigatu elkarrizketa-fitxa honen interaktiboko osagai batetik bestera Tabuladorea edo\n' + + ' Maius+Tabuladorea sakatuta.

    \n' + + '\n' + + '

    Aldatu beste elkarrizketa-fitxa batera fitxa-menua fokuratu eta dagokion Gezia\n' + + ' tekla sakatzeko, erabilgarri dauden fitxa batetik bestera txandakatzeko.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fa.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fa.js new file mode 100644 index 0000000..2a55012 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fa.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.fa', +'

    شروع پیمایش صفحه‌کلید

    \n' + + '\n' + + '
    \n' + + '
    تمرکز بر نوار منو
    \n' + + '
    Windows یا Linux:‎‏: Alt+F9
    \n' + + '
    ‎‏macOS: ⌥F9‎‏
    \n' + + '
    تمرکز بر نوار ابزار
    \n' + + '
    Windows یا Linux‎‏: Alt+F10
    \n' + + '
    ‎‏macOS: ⌥F10‎‏
    \n' + + '
    تمرکز بر پانویس
    \n' + + '
    Windows یا Linux‎‏: Alt+F11
    \n' + + '
    ‎‏macOS: ⌥F11‎‏
    \n' + + '
    تمرکز اعلان
    \n' + + '
    ویندوز یا لینوکس: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    تمرکز بر نوار ابزار بافتاری
    \n' + + '
    Windows ،Linux یا macOS:‏ Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    پیمایش در اولین مورد رابط کاربری شروع می‌شود و درخصوص اولین مورد در\n' + + ' مسیر عنصر پانویس، برجسته یا زیرخط‌دار می‌شود.

    \n' + + '\n' + + '

    پیمایش بین بخش‌های رابط کاربری

    \n' + + '\n' + + '

    برای جابجایی از یک بخش رابط کاربری به بخش بعدی، Tab را فشار دهید.

    \n' + + '\n' + + '

    برای جابجایی از یک بخش رابط کاربری به بخش قبلی، Shift+Tab را فشار دهید.

    \n' + + '\n' + + '

    ترتیب Tab این بخش‌های رابط کاربری عبارتند از:

    \n' + + '\n' + + '
      \n' + + '
    1. نوار منو
    2. \n' + + '
    3. هر گروه نوار ابزار
    4. \n' + + '
    5. نوار کناری
    6. \n' + + '
    7. مسیر عنصر در پانویس
    8. \n' + + '
    9. دکمه تغییر وضعیت تعداد کلمات در پانویس
    10. \n' + + '
    11. پیوند نمانام‌سازی در پانویس
    12. \n' + + '
    13. دسته تغییر اندازه ویرایشگر در پانویس
    14. \n' + + '
    \n' + + '\n' + + '

    اگر بخشی از رابط کاربری موجود نباشد، رد می‌شود.

    \n' + + '\n' + + '

    اگر پانویس دارای تمرکز بر پیمایش صفحه‌کلید باشد،‌ و نوار کناری قابل‌مشاهده وجود ندارد، فشردن Shift+Tab\n' + + ' تمرکز را به گروه نوار ابزار اول می‌برد، نه آخر.

    \n' + + '\n' + + '

    پیمایش در بخش‌های رابط کاربری

    \n' + + '\n' + + '

    برای جابجایی از یک عنصر رابط کاربری به بعدی، کلید جهت‌نمای مناسب را فشار دهید.

    \n' + + '\n' + + '

    کلیدهای جهت‌نمای چپ و راست

    \n' + + '\n' + + '
      \n' + + '
    • جابجایی بین منوها در نوار منو.
    • \n' + + '
    • باز کردن منوی فرعی در یک منو.
    • \n' + + '
    • جابجایی بین دکمه‌ها در یک گروه نوار ابزار.
    • \n' + + '
    • جابجایی بین موارد در مسیر عنصر پانویس.
    • \n' + + '
    \n' + + '\n' + + '

    کلیدهای جهت‌نمای پایین و بالا

    \n' + + '\n' + + '
      \n' + + '
    • جابجایی بین موارد منو در یک منو.
    • \n' + + '
    • جابجایی بین موارد در یک منوی بازشوی نوار ابزار.
    • \n' + + '
    \n' + + '\n' + + '

    کلیدهایجهت‌نما در بخش رابط کاربری متمرکز می‌چرخند.

    \n' + + '\n' + + '

    برای بستن یک منوی باز، یک منوی فرعی باز، یا یک منوی بازشوی باز، کلید Esc را فشار دهید.

    \n' + + '\n' + + '

    اگر تمرکز فعلی در «بالای» یک بخش رابط کاربری خاص است، فشردن کلید Esc نیز موجب\n' + + ' خروج کامل از پیمایش صفحه‌کلید می‌شود.

    \n' + + '\n' + + '

    اجرای یک مورد منو یا دکمه نوار ابزار

    \n' + + '\n' + + '

    وقتی مورد منو یا دکمه نوار ابزار مورد نظر هایلایت شد، دکمه بازگشت، Enter،\n' + + ' یا نوار Space را فشار دهید تا مورد را اجرا کنید.

    \n' + + '\n' + + '

    پیمایش در کادرهای گفتگوی بدون زبانه

    \n' + + '\n' + + '

    در کادرهای گفتگوی بدون زبانه، وقتی کادر گفتگو باز می‌شود، اولین جزء تعاملی متمرکز می‌شود.

    \n' + + '\n' + + '

    با فشردن Tab یا Shift+Tab، بین اجزای کادر گفتگوی تعاملی پیمایش کنید.

    \n' + + '\n' + + '

    پیمایش کادرهای گفتگوی زبانه‌دار

    \n' + + '\n' + + '

    در کادرهای گفتگوی زبانه‌دار، وقتی کادر گفتگو باز می‌شود، اولین دکمه در منوی زبانه متمرکز می‌شود.

    \n' + + '\n' + + '

    با فشردن Tab یا\n' + + ' Shift+Tab، بین اجزای تعاملی این زبانه کادر گفتگو پیمایش کنید.

    \n' + + '\n' + + '

    با دادن تمرکز به منوی زبانه و سپس فشار دادن کلید جهت‌نمای\n' + + ' مناسب برای چرخش میان زبانه‌های موجود، به زبانه کادر گفتگوی دیگری بروید.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fi.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fi.js new file mode 100644 index 0000000..f01dc91 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fi.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.fi', +'

    Näppäimistönavigoinnin aloittaminen

    \n' + + '\n' + + '
    \n' + + '
    Siirrä kohdistus valikkopalkkiin
    \n' + + '
    Windows tai Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Siirrä kohdistus työkalupalkkiin
    \n' + + '
    Windows tai Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Siirrä kohdistus alatunnisteeseen
    \n' + + '
    Windows tai Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Keskitä ilmoitukseen
    \n' + + '
    Windows ja Linux: Alt + F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Siirrä kohdistus kontekstuaaliseen työkalupalkkiin
    \n' + + '
    Windows, Linux tai macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigointi aloitetaan ensimmäisestä käyttöliittymän kohteesta, joka joko korostetaan tai alleviivataan, jos\n' + + ' kyseessä on Alatunniste-elementin polun ensimmäinen kohde.

    \n' + + '\n' + + '

    Käyttöliittymän eri osien välillä navigointi

    \n' + + '\n' + + '

    Paina sarkainnäppäintä siirtyäksesi käyttöliittymän osasta seuraavaan.

    \n' + + '\n' + + '

    Jos haluat siirtyä edelliseen käyttöliittymän osaan, paina Shift+sarkainnäppäin.

    \n' + + '\n' + + '

    Sarkainnäppäin siirtää sinua näissä käyttöliittymän osissa tässä järjestyksessä:

    \n' + + '\n' + + '
      \n' + + '
    1. Valikkopalkki
    2. \n' + + '
    3. Työkalupalkin ryhmät
    4. \n' + + '
    5. Sivupalkki
    6. \n' + + '
    7. Elementin polku alatunnisteessa
    8. \n' + + '
    9. Sanalaskurin vaihtopainike alatunnisteessa
    10. \n' + + '
    11. Brändäyslinkki alatunnisteessa
    12. \n' + + '
    13. Editorin koon muuttamisen kahva alatunnisteessa
    14. \n' + + '
    \n' + + '\n' + + '

    Jos jotakin käyttöliittymän osaa ei ole, se ohitetaan.

    \n' + + '\n' + + '

    Jos kohdistus on siirretty alatunnisteeseen näppäimistönavigoinnilla eikä sivupalkkia ole näkyvissä, Shift+sarkainnäppäin\n' + + ' siirtää kohdistuksen työkalupalkin ensimmäiseen ryhmään, eikä viimeiseen.

    \n' + + '\n' + + '

    Käyttöliittymän eri osien sisällä navigointi

    \n' + + '\n' + + '

    Paina nuolinäppäimiä siirtyäksesi käyttöliittymäelementistä seuraavaan.

    \n' + + '\n' + + '

    Vasen- ja Oikea-nuolinäppäimet

    \n' + + '\n' + + '
      \n' + + '
    • siirtävät sinua valikkopalkin valikoiden välillä.
    • \n' + + '
    • avaavat valikon alavalikon.
    • \n' + + '
    • siirtävät sinua työkalupalkin ryhmän painikkeiden välillä.
    • \n' + + '
    • siirtävät sinua kohteiden välillä alatunnisteen elementin polussa.
    • \n' + + '
    \n' + + '\n' + + '

    Alas- ja Ylös-nuolinäppäimet

    \n' + + '\n' + + '
      \n' + + '
    • siirtävät sinua valikon valikkokohteiden välillä.
    • \n' + + '
    • siirtävät sinua työkalupalkin ponnahdusvalikon kohteiden välillä.
    • \n' + + '
    \n' + + '\n' + + '

    Nuolinäppäimet siirtävät sinua käyttöliittymän korostetun osan sisällä syklissä.

    \n' + + '\n' + + '

    Paina Esc-näppäintä sulkeaksesi avoimen valikon, avataksesi alavalikon tai avataksesi ponnahdusvalikon.

    \n' + + '\n' + + '

    Jos kohdistus on käyttöliittymän tietyn osion ylälaidassa, Esc-näppäimen painaminen\n' + + ' poistuu myös näppäimistönavigoinnista kokonaan.

    \n' + + '\n' + + '

    Suorita valikkokohde tai työkalupalkin painike

    \n' + + '\n' + + '

    Kun haluamasi valikkokohde tai työkalupalkin painike on korostettuna, paina Return-, Enter-\n' + + ' tai välilyöntinäppäintä suorittaaksesi kohteen.

    \n' + + '\n' + + '

    Välilehdittömissä valintaikkunoissa navigointi

    \n' + + '\n' + + '

    Kun välilehdetön valintaikkuna avautuu, kohdistus siirtyy sen ensimmäiseen interaktiiviseen komponenttiin.

    \n' + + '\n' + + '

    Voit siirtyä valintaikkunan interaktiivisten komponenttien välillä painamalla sarkainnäppäintä tai Shift+sarkainnäppäin.

    \n' + + '\n' + + '

    Välilehdellisissä valintaikkunoissa navigointi

    \n' + + '\n' + + '

    Kun välilehdellinen valintaikkuna avautuu, kohdistus siirtyy välilehtivalikon ensimmäiseen painikkeeseen.

    \n' + + '\n' + + '

    Voit siirtyä valintaikkunan välilehden interaktiivisen komponenttien välillä painamalla sarkainnäppäintä tai\n' + + ' Shift+sarkainnäppäin.

    \n' + + '\n' + + '

    Voit siirtyä valintaikkunan toiseen välilehteen siirtämällä kohdistuksen välilehtivalikkoon ja painamalla sopivaa nuolinäppäintä\n' + + ' siirtyäksesi käytettävissä olevien välilehtien välillä syklissä.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fr_FR.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fr_FR.js new file mode 100644 index 0000000..3f611e8 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/fr_FR.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.fr_FR', +'

    Débuter la navigation au clavier

    \n' + + '\n' + + '
    \n' + + '
    Cibler la barre du menu
    \n' + + '
    Windows ou Linux : Alt+F9
    \n' + + '
    macOS : ⌥F9
    \n' + + "
    Cibler la barre d'outils
    \n" + + '
    Windows ou Linux : Alt+F10
    \n' + + '
    macOS : ⌥F10
    \n' + + '
    Cibler le pied de page
    \n' + + '
    Windows ou Linux : Alt+F11
    \n' + + '
    macOS : ⌥F11
    \n' + + '
    Cibler la notification
    \n' + + '
    Windows ou Linux : Alt+F12
    \n' + + '
    macOS : ⌥F12
    \n' + + "
    Cibler une barre d'outils contextuelle
    \n" + + '
    Windows, Linux ou macOS : Ctrl+F9
    \n' + + '
    \n' + + '\n' + + "

    La navigation débutera sur le premier élément de l'interface utilisateur, qui sera mis en surbrillance ou bien souligné dans le cas du premier élément du\n" + + " chemin d'éléments du pied de page.

    \n" + + '\n' + + "

    Naviguer entre les sections de l'interface utilisateur

    \n" + + '\n' + + "

    Pour passer d'une section de l'interface utilisateur à la suivante, appuyez sur Tabulation.

    \n" + + '\n' + + "

    Pour passer d'une section de l'interface utilisateur à la précédente, appuyez sur Maj+Tabulation.

    \n" + + '\n' + + "

    L'ordre de Tabulation de ces sections de l'interface utilisateur est le suivant :

    \n" + + '\n' + + '
      \n' + + '
    1. Barre du menu
    2. \n' + + "
    3. Chaque groupe de barres d'outils
    4. \n" + + '
    5. Barre latérale
    6. \n' + + "
    7. Chemin d'éléments du pied de page
    8. \n" + + "
    9. Bouton d'activation du compteur de mots dans le pied de page
    10. \n" + + '
    11. Lien de marque dans le pied de page
    12. \n' + + "
    13. Poignée de redimensionnement de l'éditeur dans le pied de page
    14. \n" + + '
    \n' + + '\n' + + "

    Si une section de l'interface utilisateur n'est pas présente, elle sera ignorée.

    \n" + + '\n' + + "

    Si le pied de page comporte un ciblage par navigation au clavier et qu'il n'y a aucune barre latérale visible, appuyer sur Maj+Tabulation\n" + + " déplace le ciblage vers le premier groupe de barres d'outils et non le dernier.

    \n" + + '\n' + + "

    Naviguer au sein des sections de l'interface utilisateur

    \n" + + '\n' + + "

    Pour passer d'un élément de l'interface utilisateur au suivant, appuyez sur la Flèche appropriée.

    \n" + + '\n' + + '

    Les touches fléchées Gauche et Droite

    \n' + + '\n' + + '
      \n' + + '
    • se déplacent entre les menus de la barre des menus.
    • \n' + + "
    • ouvrent un sous-menu au sein d'un menu.
    • \n" + + "
    • se déplacent entre les boutons d'un groupe de barres d'outils.
    • \n" + + "
    • se déplacent entre les éléments du chemin d'éléments du pied de page.
    • \n" + + '
    \n' + + '\n' + + '

    Les touches fléchées Bas et Haut

    \n' + + '\n' + + '
      \n' + + "
    • se déplacent entre les éléments de menu au sein d'un menu.
    • \n" + + "
    • se déplacent entre les éléments au sein d'un menu contextuel de barre d'outils.
    • \n" + + '
    \n' + + '\n' + + "

    Les Flèches parcourent la section de l'interface utilisateur ciblée.

    \n" + + '\n' + + '

    Pour fermer un menu ouvert, un sous-menu ouvert ou un menu contextuel ouvert, appuyez sur Echap.

    \n' + + '\n' + + "

    Si l'actuel ciblage se trouve en « haut » d'une section spécifique de l'interface utilisateur, appuyer sur Echap permet également de quitter\n" + + ' entièrement la navigation au clavier.

    \n' + + '\n' + + "

    Exécuter un élément de menu ou un bouton de barre d'outils

    \n" + + '\n' + + "

    Lorsque l'élément de menu ou le bouton de barre d'outils désiré est mis en surbrillance, appuyez sur la touche Retour arrière, Entrée\n" + + " ou la Barre d'espace pour exécuter l'élément.

    \n" + + '\n' + + '

    Naviguer au sein de dialogues sans onglets

    \n' + + '\n' + + "

    Dans les dialogues sans onglets, le premier composant interactif est ciblé lorsque le dialogue s'ouvre.

    \n" + + '\n' + + '

    Naviguez entre les composants du dialogue interactif en appuyant sur Tabulation ou Maj+Tabulation.

    \n' + + '\n' + + '

    Naviguer au sein de dialogues avec onglets

    \n' + + '\n' + + "

    Dans les dialogues avec onglets, le premier bouton du menu de l'onglet est ciblé lorsque le dialogue s'ouvre.

    \n" + + '\n' + + '

    Naviguez entre les composants interactifs de cet onglet de dialogue en appuyant sur Tabulation ou\n' + + ' Maj+Tabulation.

    \n' + + '\n' + + "

    Passez à un autre onglet de dialogue en ciblant le menu de l'onglet et en appuyant sur la Flèche\n" + + ' appropriée pour parcourir les onglets disponibles.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/he_IL.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/he_IL.js new file mode 100644 index 0000000..7d6513a --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/he_IL.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.he_IL', +'

    התחל ניווט במקלדת

    \n' + + '\n' + + '
    \n' + + '
    התמקד בשורת התפריטים
    \n' + + '
    Windows או Linux:‏ Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    העבר מיקוד לסרגל הכלים
    \n' + + '
    Windows או Linux:‏ Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    העבר מיקוד לכותרת התחתונה
    \n' + + '
    Windows או Linux:‏ Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    העבר מיקוד להודעה
    \n' + + '
    Windows או Linux:‏ Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    העבר מיקוד לסרגל כלים הקשרי
    \n' + + '
    Windows‏, Linux או macOS:‏ Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    הניווט יתחיל ברכיב הראשון במשך, שיודגש או שיהיה מתחתיו קו תחתון במקרה של הפריט הראשון\n' + + ' הנתיב של רכיב הכותרת התחתונה.

    \n' + + '\n' + + '

    עבור בין מקטעים במסך

    \n' + + '\n' + + '

    כדי לעבור בין המקטעים במסך, הקש Tab.

    \n' + + '\n' + + '

    כדי לעבור למקטע הקודם במסך, הקש Shift+Tab.

    \n' + + '\n' + + '

    הסדר מבחינת מקש Tab של הרכיבים במסך:

    \n' + + '\n' + + '
      \n' + + '
    1. שורת התפריטים
    2. \n' + + '
    3. כל קבוצה בסרגל הכלים
    4. \n' + + '
    5. הסרגל הצידי
    6. \n' + + '
    7. נתיב של רכיב בכותרת התחתונה
    8. \n' + + '
    9. לחצן לספירת מילים בכותרת התחתונה
    10. \n' + + '
    11. קישור של המותג בכותרת התחתונה
    12. \n' + + '
    13. ידית לשינוי גודל עבור העורך בכותרת התחתונה
    14. \n' + + '
    \n' + + '\n' + + '

    אם רכיב כלשהו במסך לא מופיע, המערכת תדלג עליו.

    \n' + + '\n' + + '

    אם בכותרת התחתונה יש מיקוד של ניווט במקלדת, ולא מופיע סרגל בצד, יש להקיש Shift+Tab\n' + + ' מעביר את המיקוד לקבוצה הראשונה בסרגל הכלים, לא האחרונה.

    \n' + + '\n' + + '

    עבור בתוך מקטעים במסך

    \n' + + '\n' + + '

    כדי לעבור מרכיב אחד לרכיב אחר במסך, הקש על מקש החץ המתאים.

    \n' + + '\n' + + '

    מקשי החיצים שמאלה וימינה

    \n' + + '\n' + + '
      \n' + + '
    • עבור בין תפריטים בשורת התפריטים.
    • \n' + + '
    • פתח תפריט משני בתפריט.
    • \n' + + '
    • עבור בין לחצנים בקבוצה בסרגל הכלים.
    • \n' + + '
    • עבור בין פריטים ברכיב בכותרת התחתונה.
    • \n' + + '
    \n' + + '\n' + + '

    מקשי החיצים למטה ולמעלה

    \n' + + '\n' + + '
      \n' + + '
    • עבור בין פריטים בתפריט.
    • \n' + + '
    • עבור בין פריטים בחלון הקובץ של סרגל הכלים.
    • \n' + + '
    \n' + + '\n' + + '

    מקשי החצים משתנים בתוך המקטע במסך שעליו נמצא המיקוד.

    \n' + + '\n' + + '

    כדי לסגור תפריט פתוח, תפריט משני פתוח או חלון קופץ, הקש על Esc.

    \n' + + '\n' + + "

    אם המיקוד הוא על החלק 'העליון' של מקטע מסוים במסך, הקשה על Esc מביאה גם ליציאה\n" + + ' מהניווט במקלדת לחלוטין.

    \n' + + '\n' + + '

    הפעל פריט בתפריט או לחצן בסרגל הכלים

    \n' + + '\n' + + '

    כאשר הפריט הרצוי בתפריט או הלחצן בסרגל הכלים מודגשים, הקש על Return, Enter,\n' + + ' או על מקש הרווח כדי להפעיל את הפריט.

    \n' + + '\n' + + '

    ניווט בחלונות דו-שיח בלי כרטיסיות

    \n' + + '\n' + + '

    בחלונות דו-שיח בלי כרטיסיות, הרכיב האינטראקטיבי הראשון מקבל את המיקוד כאשר החלון נפתח.

    \n' + + '\n' + + '

    עבור בין רכיבים אינטראקטיביים בחלון על ידי הקשה על Tab או Shift+Tab.

    \n' + + '\n' + + '

    ניווט בחלונות דו-שיח עם כרטיסיות

    \n' + + '\n' + + '

    בחלונות דו-שיח עם כרטיסיות, הלחצן הראשון בתפריט מקבל את המיקוד כאשר החלון נפתח.

    \n' + + '\n' + + '

    עבור בין רכיבים אינטראקטיביים בחלון על ידי הקשה על Tab או\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    עבור לכרטיסיה אחרת בחלון על ידי העברת המיקוד לתפריט הכרטיסיות והקשה על החץהמתאים\n' + + ' כדי לעבור בין הכרטיסיות הזמינות.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hi.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hi.js new file mode 100644 index 0000000..ef59a5c --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hi.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.hi', +'

    कीबोर्ड नेविगेशन शुरू करें

    \n' + + '\n' + + '
    \n' + + '
    मेन्यू बार पर फ़ोकस करें
    \n' + + '
    Windows या Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    टूलबार पर फ़ोकस करें
    \n' + + '
    Windows या Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    फ़ुटर पर फ़ोकस करें
    \n' + + '
    Windows या Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    नोटिफ़िकेशन फ़ोकस
    \n' + + '
    Windows या Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    प्रासंगिक टूलबार पर फ़ोकस करें
    \n' + + '
    Windows, Linux या macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    नेविगेशन पहले UI आइटम पर शुरू होगा, जिसे हाइलाइट किया जाएगा या पहले आइटम के मामले में फ़ुटर तत्व पथ में\n' + + ' रेखांकित किया जाएगा।

    \n' + + '\n' + + '

    UI सेक्शन के बीच नेविगेट करें

    \n' + + '\n' + + '

    एक UI सेक्शन से दूसरे सेक्शन में जाने के लिए, Tab दबाएं।

    \n' + + '\n' + + '

    एक UI सेक्शन से पिछले सेक्शन में जाने के लिए, Shift+Tab दबाएं।

    \n' + + '\n' + + '

    इन UI सेक्शन का Tab क्रम नीचे दिया गया है:

    \n' + + '\n' + + '
      \n' + + '
    1. मेन्यू बार
    2. \n' + + '
    3. प्रत्येक टूलबार समूह
    4. \n' + + '
    5. साइडबार
    6. \n' + + '
    7. फ़ुटर में तत्व पथ
    8. \n' + + '
    9. फ़ुटर में शब्द गणना टॉगल बटन
    10. \n' + + '
    11. फ़ुटर में ब्रांडिंग लिंक
    12. \n' + + '
    13. फ़ुटर में संपादक का आकार बदलने का हैंडल
    14. \n' + + '
    \n' + + '\n' + + '

    अगर कोई UI सेक्शन मौजूद नहीं है, तो उसे छोड़ दिया जाता है।

    \n' + + '\n' + + '

    अगर फ़ुटर में कीबोर्ड नेविगेशन फ़ोकस है, और कोई दिखा देने वाला साइडबार नहीं है, तो Shift+Tab दबाने से\n' + + ' फ़ोकस पहले टूलबार समूह पर चला जाता है, पिछले पर नहीं।

    \n' + + '\n' + + '

    UI सेक्शन के भीतर नेविगेट करें

    \n' + + '\n' + + '

    एक UI तत्व से दूसरे में जाने के लिए उपयुक्त ऐरो कुंजी दबाएं।

    \n' + + '\n' + + '

    बाएं और दाएं ऐरो कुंजियां

    \n' + + '\n' + + '
      \n' + + '
    • मेन्यू बार में मेन्यू के बीच ले जाती हैं।
    • \n' + + '
    • मेन्यू में एक सब-मेन्यू खोलें।
    • \n' + + '
    • टूलबार समूह में बटनों के बीच ले जाएं।
    • \n' + + '
    • फ़ुटर के तत्व पथ में आइटम के बीच ले जाएं।
    • \n' + + '
    \n' + + '\n' + + '

    नीचे और ऊपर ऐरो कुंजियां

    \n' + + '\n' + + '
      \n' + + '
    • मेन्यू में मेन्यू आइटम के बीच ले जाती हैं।
    • \n' + + '
    • टूलबार पॉप-अप मेन्यू में आइटम के बीच ले जाएं।
    • \n' + + '
    \n' + + '\n' + + '

    फ़ोकस वाले UI सेक्शन के भीतर ऐरो कुंजियां चलाती रहती हैं।

    \n' + + '\n' + + '

    कोई खुला मेन्यू, कोई खुला सब-मेन्यू या कोई खुला पॉप-अप मेन्यू बंद करने के लिए Esc कुंजी दबाएं।

    \n' + + '\n' + + "

    अगर मौजूदा फ़ोकस किसी विशेष UI सेक्शन के 'शीर्ष' पर है, तो Esc कुंजी दबाने से भी\n" + + ' कीबोर्ड नेविगेशन पूरी तरह से बाहर हो जाता है।

    \n' + + '\n' + + '

    मेन्यू आइटम या टूलबार बटन निष्पादित करें

    \n' + + '\n' + + '

    जब वांछित मेन्यू आइटम या टूलबार बटन हाइलाइट किया जाता है, तो आइटम को निष्पादित करने के लिए Return, Enter,\n' + + ' या Space bar दबाएं।

    \n' + + '\n' + + '

    गैर-टैब वाले डायलॉग पर नेविगेट करें

    \n' + + '\n' + + '

    गैर-टैब वाले डायलॉग में, डायलॉग खुलने पर पहला इंटरैक्टिव घटक फ़ोकस लेता है।

    \n' + + '\n' + + '

    Tab or Shift+Tab दबाकर इंटरैक्टिव डायलॉग घटकों के बीच नेविगेट करें।

    \n' + + '\n' + + '

    टैब किए गए डायलॉग पर नेविगेट करें

    \n' + + '\n' + + '

    टैब किए गए डायलॉग में, डायलॉग खुलने पर टैब मेन्यू में पहला बटन फ़ोकस लेता है।

    \n' + + '\n' + + '

    इस डायलॉग टैब के इंटरैक्टिव घटकों के बीच नेविगेट करने के लिए Tab या\n' + + ' Shift+Tab दबाएं।

    \n' + + '\n' + + '

    टैब मेन्यू को फ़ोकस देकर और फिर उपलब्ध टैब में के बीच जाने के लिए उपयुक्त ऐरो\n' + + ' कुंजी दबाकर दूसरे डायलॉग टैब पर स्विच करें।

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hr.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hr.js new file mode 100644 index 0000000..1bf35c5 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hr.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.hr', +'

    Početak navigacije na tipkovnici

    \n' + + '\n' + + '
    \n' + + '
    Fokusiranje trake izbornika
    \n' + + '
    Windows ili Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokusiranje alatne trake
    \n' + + '
    Windows ili Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokusiranje podnožja
    \n' + + '
    Windows ili Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Fokusiranje obavijesti
    \n' + + '
    Windows ili Linux: Alt + F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokusiranje kontekstne alatne trake
    \n' + + '
    Windows, Linux ili macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigacija će započeti kod prve stavke na korisničkom sučelju, koja će biti istaknuta ili podcrtana ako se radi o prvoj stavci u\n' + + ' putu elementa u podnožju.

    \n' + + '\n' + + '

    Navigacija između dijelova korisničkog sučelja

    \n' + + '\n' + + '

    Za pomicanje s jednog dijela korisničkog sučelja na drugi pritisnite tabulator.

    \n' + + '\n' + + '

    Za pomicanje s jednog dijela korisničkog sučelja na prethodni pritisnite Shift + tabulator.

    \n' + + '\n' + + '

    Ovo je redoslijed pomicanja tabulatora po dijelovima korisničkog sučelja:

    \n' + + '\n' + + '
      \n' + + '
    1. Traka izbornika
    2. \n' + + '
    3. Pojedinačne grupe na alatnoj traci
    4. \n' + + '
    5. Bočna traka
    6. \n' + + '
    7. Put elemenata u podnožju
    8. \n' + + '
    9. Gumb za pomicanje po broju riječi u podnožju
    10. \n' + + '
    11. Veza na brand u podnožju
    12. \n' + + '
    13. Značajka za promjenu veličine alata za uređivanje u podnožju
    14. \n' + + '
    \n' + + '\n' + + '

    Ako neki dio korisničkog sučelja nije naveden, on se preskače.

    \n' + + '\n' + + '

    Ako u podnožju postoji fokus za navigaciju na tipkovnici, a nema vidljive bočne trake, pritiskom na Shift + tabulator\n' + + ' fokus se prebacuje na prvu skupinu na alatnoj traci, ne na zadnju.

    \n' + + '\n' + + '

    Navigacija unutar dijelova korisničkog sučelja

    \n' + + '\n' + + '

    Za pomicanje s jednog elementa korisničkog sučelja na drugi pritisnite tipku s odgovarajućom strelicom.

    \n' + + '\n' + + '

    Tipke s lijevom i desnom strelicom

    \n' + + '\n' + + '
      \n' + + '
    • služe za pomicanje između izbornika na alatnoj traci.
    • \n' + + '
    • otvaraju podizbornik unutar izbornika.
    • \n' + + '
    • služe za pomicanje između gumba unutar skupina na alatnoj traci.
    • \n' + + '
    • služe za pomicanje između stavki na elementu puta u podnožju.
    • \n' + + '
    \n' + + '\n' + + '

    Tipke s donjom i gornjom strelicom

    \n' + + '\n' + + '
      \n' + + '
    • služe za pomicanje između stavki unutar izbornika.
    • \n' + + '
    • služe za pomicanje između stavki na alatnoj traci skočnog izbornika.
    • \n' + + '
    \n' + + '\n' + + '

    Tipkama strelica kružno se pomičete unutar dijela korisničkog sučelja koji je u fokusu.

    \n' + + '\n' + + '

    Za zatvaranje otvorenog izbornika, otvorenog podizbornika ili otvorenog skočnog izbornika pritisnite tipku Esc.

    \n' + + '\n' + + '

    Ako je fokus trenutačno postavljen na vrh pojedinačnog dijela korisničkog sučelja, pritiskom na tipku Esc također\n' + + ' u potpunosti zatvarate navigaciju na tipkovnici.

    \n' + + '\n' + + '

    Izvršavanje radnji putem stavki izbornika ili gumba na alatnoj traci

    \n' + + '\n' + + '

    Nakon što se istakne stavka izbornika ili gumb na alatnoj traci s radnjom koju želite izvršiti, pritisnite tipku Return, Enter\n' + + ' ili razmak da biste pokrenuli željenu radnju.

    \n' + + '\n' + + '

    Navigacija dijaloškim okvirima izvan kartica

    \n' + + '\n' + + '

    Prilikom otvaranja dijaloških okvira izvan kartica fokus se nalazi na prvoj interaktivnoj komponenti.

    \n' + + '\n' + + '

    Navigaciju između interaktivnih dijaloških komponenata vršite pritiskom na tabulator ili Shift + tabulator.

    \n' + + '\n' + + '

    Navigacija dijaloškim okvirima u karticama

    \n' + + '\n' + + '

    Prilikom otvaranja dijaloških okvira u karticama fokus se nalazi na prvom gumbu u izborniku unutar kartice.

    \n' + + '\n' + + '

    Navigaciju između interaktivnih komponenata dijaloškog okvira u kartici vršite pritiskom na tabulator ili\n' + + ' Shift + tabulator.

    \n' + + '\n' + + '

    Na karticu s drugim dijaloškim okvirom možete se prebaciti tako da stavite fokus na izbornik kartice pa pritisnete tipku s odgovarajućom strelicom\n' + + ' za kružno pomicanje između dostupnih kartica.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hu_HU.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hu_HU.js new file mode 100644 index 0000000..5c984bb --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/hu_HU.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.hu_HU', +'

    Billentyűzetes navigáció indítása

    \n' + + '\n' + + '
    \n' + + '
    Fókusz a menüsávra
    \n' + + '
    Windows és Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fókusz az eszköztárra
    \n' + + '
    Windows és Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fókusz a láblécre
    \n' + + '
    Windows és Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Ráközelítés az értesítésre
    \n' + + '
    Windows vagy Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fókusz egy környezetfüggő eszköztárra
    \n' + + '
    Windows, Linux és macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    A navigáció az első felhasználói felületi elemnél kezdődik, amelyet a rendszer kiemel, illetve aláhúz, amennyiben az az első elem\n' + + ' a lábléc elemútvonalán.

    \n' + + '\n' + + '

    Navigálás a felhasználói felület szakaszai között

    \n' + + '\n' + + '

    A felhasználói felület következő szakaszára váltáshoz nyomja meg a Tab billentyűt.

    \n' + + '\n' + + '

    A felhasználói felület előző szakaszára váltáshoz nyomja meg a Shift+Tab billentyűt.

    \n' + + '\n' + + '

    A Tab billentyűvel a felhasználói felület szakaszai között a következő sorrendben vált:

    \n' + + '\n' + + '
      \n' + + '
    1. Menüsáv
    2. \n' + + '
    3. Az egyes eszköztárcsoportok
    4. \n' + + '
    5. Oldalsáv
    6. \n' + + '
    7. Elemútvonal a láblécen
    8. \n' + + '
    9. Szószámátkapcsoló gomb a láblécen
    10. \n' + + '
    11. Márkalink a láblécen
    12. \n' + + '
    13. Szerkesztő átméretezési fogópontja a láblécen
    14. \n' + + '
    \n' + + '\n' + + '

    Ha a felhasználói felület valamelyik eleme nincs jelen, a rendszer kihagyja.

    \n' + + '\n' + + '

    Ha a billentyűzetes navigáció fókusza a láblécen van, és nincs látható oldalsáv, a Shift+Tab\n' + + ' billentyűkombináció lenyomásakor az első eszköztárcsoportra ugrik a fókusz, nem az utolsóra.

    \n' + + '\n' + + '

    Navigálás a felhasználói felület szakaszain belül

    \n' + + '\n' + + '

    A felhasználói felület következő elemére váltáshoz nyomja meg a megfelelő nyílbillentyűt.

    \n' + + '\n' + + '

    A bal és a jobb nyílgomb

    \n' + + '\n' + + '
      \n' + + '
    • a menüsávban a menük között vált.
    • \n' + + '
    • a menükben megnyit egy almenüt.
    • \n' + + '
    • az eszköztárcsoportban a gombok között vált.
    • \n' + + '
    • a lábléc elemútvonalán az elemek között vált.
    • \n' + + '
    \n' + + '\n' + + '

    A le és a fel nyílgomb

    \n' + + '\n' + + '
      \n' + + '
    • a menükben a menüpontok között vált.
    • \n' + + '
    • az eszköztár előugró menüjében az elemek között vált.
    • \n' + + '
    \n' + + '\n' + + '

    A nyílbillentyűk lenyomásával körkörösen lépkedhet a fókuszban lévő felhasználói felületi szakasz elemei között.

    \n' + + '\n' + + '

    A megnyitott menüket, almenüket és előugró menüket az Esc billentyűvel zárhatja be.

    \n' + + '\n' + + '

    Ha a fókusz az aktuális felületi elem „felső” részén van, az Esc billentyűvel az egész\n' + + ' billentyűzetes navigációból kilép.

    \n' + + '\n' + + '

    Menüpont vagy eszköztárgomb aktiválása

    \n' + + '\n' + + '

    Amikor a kívánt menüelem vagy eszköztárgomb van kijelölve, nyomja meg a Return, az Enter\n' + + ' vagy a Szóköz billentyűt az adott elem vagy gomb aktiválásához.

    \n' + + '\n' + + '

    Navigálás a lapokkal nem rendelkező párbeszédablakokban

    \n' + + '\n' + + '

    A lapokkal nem rendelkező párbeszédablakokban az első interaktív összetevő kapja a fókuszt, amikor a párbeszédpanel megnyílik.

    \n' + + '\n' + + '

    A párbeszédpanelek interaktív összetevői között a Tab vagy a Shift+Tab billentyűvel navigálhat.

    \n' + + '\n' + + '

    Navigálás a lapokkal rendelkező párbeszédablakokban

    \n' + + '\n' + + '

    A lapokkal rendelkező párbeszédablakokban a lapmenü első gombja kapja a fókuszt, amikor a párbeszédpanel megnyílik.

    \n' + + '\n' + + '

    A párbeszédpanel e lapjának interaktív összetevői között a Tab vagy\n' + + ' Shift+Tab billentyűvel navigálhat.

    \n' + + '\n' + + '

    A párbeszédablak másik lapjára úgy léphet, hogy a fókuszt a lapmenüre állítja, majd lenyomja a megfelelő nyílbillentyűt\n' + + ' a rendelkezésre álló lapok közötti lépkedéshez.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/id.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/id.js new file mode 100644 index 0000000..d607dd1 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/id.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.id', +'

    Memulai navigasi keyboard

    \n' + + '\n' + + '
    \n' + + '
    Fokus pada bilah Menu
    \n' + + '
    Windows atau Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokus pada Bilah Alat
    \n' + + '
    Windows atau Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokus pada footer
    \n' + + '
    Windows atau Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Fokuskan pemberitahuan
    \n' + + '
    Windows atau Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokus pada bilah alat kontekstual
    \n' + + '
    Windows, Linux, atau macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigasi akan dimulai dari item pertama UI, yang akan disorot atau digarisbawahi di\n' + + ' alur elemen Footer.

    \n' + + '\n' + + '

    Berpindah antar-bagian UI

    \n' + + '\n' + + '

    Untuk berpindah dari satu bagian UI ke bagian berikutnya, tekan Tab.

    \n' + + '\n' + + '

    Untuk berpindah dari satu bagian UI ke bagian sebelumnya, tekan Shift+Tab.

    \n' + + '\n' + + '

    Urutan Tab bagian-bagian UI ini adalah:

    \n' + + '\n' + + '
      \n' + + '
    1. Bilah menu
    2. \n' + + '
    3. Tiap grup bilah alat
    4. \n' + + '
    5. Bilah sisi
    6. \n' + + '
    7. Alur elemen di footer
    8. \n' + + '
    9. Tombol aktifkan/nonaktifkan jumlah kata di footer
    10. \n' + + '
    11. Tautan merek di footer
    12. \n' + + '
    13. Pengatur pengubahan ukuran editor di footer
    14. \n' + + '
    \n' + + '\n' + + '

    Jika suatu bagian UI tidak ada, bagian tersebut dilewati.

    \n' + + '\n' + + '

    Jika fokus navigasi keyboard ada pada footer, tetapi tidak ada bilah sisi yang terlihat, menekan Shift+Tab\n' + + ' akan memindahkan fokus ke grup bilah alat pertama, bukan yang terakhir.

    \n' + + '\n' + + '

    Berpindah di dalam bagian-bagian UI

    \n' + + '\n' + + '

    Untuk berpindah dari satu elemen UI ke elemen berikutnya, tekan tombol Panah yang sesuai.

    \n' + + '\n' + + '

    Tombol panah Kiri dan Kanan untuk

    \n' + + '\n' + + '
      \n' + + '
    • berpindah-pindah antar-menu di dalam bilah menu.
    • \n' + + '
    • membuka sub-menu di dalam menu.
    • \n' + + '
    • berpindah-pindah antar-tombol di dalam grup bilah alat.
    • \n' + + '
    • berpindah-pindah antar-item di dalam alur elemen footer.
    • \n' + + '
    \n' + + '\n' + + '

    Tombol panah Bawah dan Atas untuk

    \n' + + '\n' + + '
      \n' + + '
    • berpindah-pindah antar-item menu di dalam menu.
    • \n' + + '
    • berpindah-pindah antar-item di dalam menu pop-up bilah alat.
    • \n' + + '
    \n' + + '\n' + + '

    Tombol Panah hanya bergerak di dalam bagian UI yang difokuskan.

    \n' + + '\n' + + '

    Untuk menutup menu, sub-menu, atau menu pop-up yang terbuka, tekan tombol Esc.

    \n' + + '\n' + + '

    Jika fokus sedang berada di ‘atas’ bagian UI tertentu, menekan tombol Esc juga dapat mengeluarkan fokus\n' + + ' dari seluruh navigasi keyboard.

    \n' + + '\n' + + '

    Menjalankan item menu atau tombol bilah alat

    \n' + + '\n' + + '

    Jika item menu atau tombol bilah alat yang diinginkan tersorot, tekan Return, Enter,\n' + + ' atau Spasi untuk menjalankan item.

    \n' + + '\n' + + '

    Berpindah dalam dialog tanpa tab

    \n' + + '\n' + + '

    Dalam dialog tanpa tab, fokus diarahkan pada komponen interaktif pertama saat dialog terbuka.

    \n' + + '\n' + + '

    Berpindah di antara komponen dalam dialog interaktif dengan menekan Tab atau Shift+Tab.

    \n' + + '\n' + + '

    Berpindah dalam dialog dengan tab

    \n' + + '\n' + + '

    Dalam dialog yang memiliki tab, fokus diarahkan pada tombol pertama di dalam menu saat dialog terbuka.

    \n' + + '\n' + + '

    Berpindah di antara komponen-komponen interaktif pada tab dialog ini dengan menekan Tab atau\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Beralih ke tab dialog lain dengan mengarahkan fokus pada menu tab lalu tekan tombol Panah\n' + + ' yang sesuai untuk berpindah ke berbagai tab yang tersedia.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/it.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/it.js new file mode 100644 index 0000000..3a791c9 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/it.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.it', +'

    Iniziare la navigazione tramite tastiera

    \n' + + '\n' + + '
    \n' + + '
    Impostare lo stato attivo per la barra dei menu
    \n' + + '
    Windows o Linux: ALT+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Impostare lo stato attivo per la barra degli strumenti
    \n' + + '
    Windows o Linux: ALT+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Impostare lo stato attivo per il piè di pagina
    \n' + + '
    Windows o Linux: ALT+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Metti a fuoco la notifica
    \n' + + '
    Windows o Linux: ALT+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Impostare lo stato attivo per la barra degli strumenti contestuale
    \n' + + '
    Windows, Linux o macOS: CTRL+F9
    \n' + + '
    \n' + + '\n' + + "

    La navigazione inizierà dalla prima voce dell'interfaccia utente, che sarà evidenziata o sottolineata nel caso della prima voce\n" + + " nel percorso dell'elemento del piè di pagina.

    \n" + + '\n' + + "

    Navigare tra le sezioni dell'interfaccia utente

    \n" + + '\n' + + "

    Per passare da una sezione dell'interfaccia utente alla successiva, premere TAB.

    \n" + + '\n' + + "

    Per passare da una sezione dell'interfaccia utente alla precedente, premere MAIUSC+TAB.

    \n" + + '\n' + + "

    L'ordine di tabulazione di queste sezioni dell'interfaccia utente è:

    \n" + + '\n' + + '
      \n' + + '
    1. Barra dei menu
    2. \n' + + '
    3. Ogni gruppo di barre degli strumenti
    4. \n' + + '
    5. Barra laterale
    6. \n' + + "
    7. Percorso dell'elemento nel piè di pagina
    8. \n" + + '
    9. Pulsante di attivazione/disattivazione del conteggio delle parole nel piè di pagina
    10. \n' + + '
    11. Collegamento al marchio nel piè di pagina
    12. \n' + + "
    13. Quadratino di ridimensionamento dell'editor nel piè di pagina
    14. \n" + + '
    \n' + + '\n' + + "

    Se una sezione dell'interfaccia utente non è presente, viene saltata.

    \n" + + '\n' + + '

    Se il piè di pagina ha lo stato attivo per la navigazione tramite tastiera e non è presente alcuna barra laterale visibile, premendo MAIUSC+TAB\n' + + " si sposta lo stato attivo sul primo gruppo di barre degli strumenti, non sull'ultimo.

    \n" + + '\n' + + "

    Navigare all'interno delle sezioni dell'interfaccia utente

    \n" + + '\n' + + "

    Per passare da un elemento dell'interfaccia utente al successivo, premere il tasto freccia appropriato.

    \n" + + '\n' + + '

    I tasti freccia Sinistra e Destra

    \n' + + '\n' + + '
      \n' + + '
    • consentono di spostarsi tra i menu della barra dei menu.
    • \n' + + '
    • aprono un sottomenu in un menu.
    • \n' + + '
    • consentono di spostarsi tra i pulsanti di un gruppo di barre degli strumenti.
    • \n' + + "
    • consentono di spostarsi tra le voci nel percorso dell'elemento del piè di pagina.
    • \n" + + '
    \n' + + '\n' + + '

    I tasti freccia Giù e Su

    \n' + + '\n' + + '
      \n' + + '
    • consentono di spostarsi tra le voci di un menu.
    • \n' + + '
    • consentono di spostarsi tra le voci di un menu a comparsa della barra degli strumenti.
    • \n' + + '
    \n' + + '\n' + + "

    I tasti freccia consentono di spostarsi all'interno della sezione dell'interfaccia utente con stato attivo.

    \n" + + '\n' + + '

    Per chiudere un menu aperto, un sottomenu aperto o un menu a comparsa aperto, premere il tasto ESC.

    \n' + + '\n' + + "

    Se lo stato attivo corrente si trova nella parte superiore di una particolare sezione dell'interfaccia utente, premendo il tasto ESC si esce\n" + + ' completamente dalla navigazione tramite tastiera.

    \n' + + '\n' + + '

    Eseguire una voce di menu o un pulsante della barra degli strumenti

    \n' + + '\n' + + '

    Quando la voce di menu o il pulsante della barra degli strumenti desiderati sono evidenziati, premere il tasto diritorno a capo, il tasto Invio\n' + + ' o la barra spaziatrice per eseguirli.

    \n' + + '\n' + + '

    Navigare nelle finestre di dialogo non a schede

    \n' + + '\n' + + "

    Nelle finestre di dialogo non a schede, all'apertura della finestra di dialogo diventa attivo il primo componente interattivo.

    \n" + + '\n' + + '

    Per spostarsi tra i componenti interattivi della finestra di dialogo, premere TAB o MAIUSC+TAB.

    \n' + + '\n' + + '

    Navigare nelle finestre di dialogo a schede

    \n' + + '\n' + + "

    Nelle finestre di dialogo a schede, all'apertura della finestra di dialogo diventa attivo il primo pulsante del menu della scheda.

    \n" + + '\n' + + '

    Per spostarsi tra i componenti interattivi di questa scheda della finestra di dialogo, premere TAB o\n' + + ' MAIUSC+TAB.

    \n' + + '\n' + + "

    Per passare a un'altra scheda della finestra di dialogo, attivare il menu della scheda e premere il tasto freccia\n" + + ' appropriato per scorrere le schede disponibili.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ja.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ja.js new file mode 100644 index 0000000..26872db --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ja.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.ja', +'

    キーボード ナビゲーションの開始

    \n' + + '\n' + + '
    \n' + + '
    メニュー バーをフォーカス
    \n' + + '
    Windows または Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    ツール バーをフォーカス
    \n' + + '
    Windows または Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    フッターをフォーカス
    \n' + + '
    Windows または Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    通知にフォーカス
    \n' + + '
    Windows または Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    コンテキスト ツール バーをフォーカス
    \n' + + '
    Windows、Linux または macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    ナビゲーションは最初の UI 項目から開始され、強調表示されるか、フッターの要素パスにある最初の項目の場合は\n' + + ' 下線が引かれます。

    \n' + + '\n' + + '

    UI セクション間の移動

    \n' + + '\n' + + '

    次の UI セクションに移動するには、Tab を押します。

    \n' + + '\n' + + '

    前の UI セクションに移動するには、Shift+Tab を押します。

    \n' + + '\n' + + '

    これらの UI セクションの Tab の順序:

    \n' + + '\n' + + '
      \n' + + '
    1. メニュー バー
    2. \n' + + '
    3. 各ツール バー グループ
    4. \n' + + '
    5. サイド バー
    6. \n' + + '
    7. フッターの要素パス
    8. \n' + + '
    9. フッターの単語数切り替えボタン
    10. \n' + + '
    11. フッターのブランド リンク
    12. \n' + + '
    13. フッターのエディター サイズ変更ハンドル
    14. \n' + + '
    \n' + + '\n' + + '

    UI セクションが存在しない場合は、スキップされます。

    \n' + + '\n' + + '

    フッターにキーボード ナビゲーション フォーカスがあり、表示可能なサイド バーがない場合、Shift+Tab を押すと、\n' + + ' フォーカスが最後ではなく最初のツール バー グループに移動します。

    \n' + + '\n' + + '

    UI セクション内の移動

    \n' + + '\n' + + '

    次の UI 要素に移動するには、適切な矢印キーを押します。

    \n' + + '\n' + + '

    左矢印右矢印のキー

    \n' + + '\n' + + '
      \n' + + '
    • メニュー バーのメニュー間で移動します。
    • \n' + + '
    • メニュー内のサブメニューを開きます。
    • \n' + + '
    • ツール バー グループのボタン間で移動します。
    • \n' + + '
    • フッターの要素パスの項目間で移動します。
    • \n' + + '
    \n' + + '\n' + + '

    下矢印上矢印のキー

    \n' + + '\n' + + '
      \n' + + '
    • メニュー内のメニュー項目間で移動します。
    • \n' + + '
    • ツール バー ポップアップ メニュー内のメニュー項目間で移動します。
    • \n' + + '
    \n' + + '\n' + + '

    矢印キーで、フォーカスされた UI セクション内で循環します。

    \n' + + '\n' + + '

    開いたメニュー、開いたサブメニュー、開いたポップアップ メニューを閉じるには、Esc キーを押します。

    \n' + + '\n' + + '

    現在のフォーカスが特定の UI セクションの「一番上」にある場合、Esc キーを押すと\n' + + ' キーボード ナビゲーションも完全に閉じられます。

    \n' + + '\n' + + '

    メニュー項目またはツール バー ボタンの実行

    \n' + + '\n' + + '

    目的のメニュー項目やツール バー ボタンが強調表示されている場合、リターンEnter、\n' + + ' またはスペース キーを押して項目を実行します。

    \n' + + '\n' + + '

    タブのないダイアログの移動

    \n' + + '\n' + + '

    タブのないダイアログでは、ダイアログが開くと最初の対話型コンポーネントがフォーカスされます。

    \n' + + '\n' + + '

    Tab または Shift+Tab を押して、対話型ダイアログ コンポーネント間で移動します。

    \n' + + '\n' + + '

    タブ付きダイアログの移動

    \n' + + '\n' + + '

    タブ付きダイアログでは、ダイアログが開くとタブ メニューの最初のボタンがフォーカスされます。

    \n' + + '\n' + + '

    Tab または\n' + + ' Shift+Tab を押して、このダイアログ タブの対話型コンポーネント間で移動します。

    \n' + + '\n' + + '

    タブ メニューをフォーカスしてから適切な矢印キーを押して表示可能なタブを循環して、\n' + + ' 別のダイアログに切り替えます。

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/kk.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/kk.js new file mode 100644 index 0000000..e31532f --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/kk.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.kk', +'

    Пернетақта навигациясын бастау

    \n' + + '\n' + + '
    \n' + + '
    Мәзір жолағын фокустау
    \n' + + '
    Windows немесе Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Құралдар тақтасын фокустау
    \n' + + '
    Windows немесе Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Төменгі деректемені фокустау
    \n' + + '
    Windows немесе Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Хабарландыруды белгілеу
    \n' + + '
    Windows немесе Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Мәтінмәндік құралдар тақтасын фокустау
    \n' + + '
    Windows, Linux немесе macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Навигация бөлектелетін немесе Төменгі деректеме элементінің жолындағы бірінші элемент жағдайында асты сызылатын\n' + + ' бірінші ПИ элементінен басталады.

    \n' + + '\n' + + '

    ПИ бөлімдері арасында навигациялау

    \n' + + '\n' + + '

    Бір ПИ бөлімінен келесісіне өту үшін Tab пернесін басыңыз.

    \n' + + '\n' + + '

    Бір ПИ бөлімінен алдыңғысына өту үшін Shift+Tab пернесін басыңыз.

    \n' + + '\n' + + '

    Осы ПИ бөлімдерінің Tab реті:

    \n' + + '\n' + + '
      \n' + + '
    1. Мәзір жолағы
    2. \n' + + '
    3. Әрбір құралдар тақтасы тобы
    4. \n' + + '
    5. Бүйірлік жолақ
    6. \n' + + '
    7. Төменгі деректемедегі элемент жолы
    8. \n' + + '
    9. Төменгі деректемедегі сөздер санын ауыстыру түймесі
    10. \n' + + '
    11. Төменгі деректемедегі брендингтік сілтеме
    12. \n' + + '
    13. Төменгі деректемедегі редактор өлшемін өзгерту тұтқасы
    14. \n' + + '
    \n' + + '\n' + + '

    ПИ бөлімі көрсетілмесе, ол өткізіп жіберіледі.

    \n' + + '\n' + + '

    Төменгі деректемеде пернетақта навигациясының фокусы болса және бүйірлік жолақ көрінбесе, Shift+Tab тіркесімін басу әрекеті\n' + + ' фокусты соңғысы емес, бірінші құралдар тақтасы тобына жылжытады.

    \n' + + '\n' + + '

    ПИ бөлімдерінде навигациялау

    \n' + + '\n' + + '

    Бір ПИ элементінен келесісіне өту үшін Arrow (Көрсеткі) пернесін басыңыз.

    \n' + + '\n' + + '

    Left (Сол жақ) және Right (Оң жақ) көрсеткі пернелері

    \n' + + '\n' + + '
      \n' + + '
    • мәзір жолағындағы мәзірлер арасында жылжыту.
    • \n' + + '
    • мәзірде ішкі мәзірді ашу.
    • \n' + + '
    • құралдар тақтасы тобындағы түймелер арасында жылжыту.
    • \n' + + '
    • төменгі деректеме элементінің жолындағы элементтер арасында жылжыту.
    • \n' + + '
    \n' + + '\n' + + '

    Down (Төмен) және Up (Жоғары) көрсеткі пернелері

    \n' + + '\n' + + '
      \n' + + '
    • мәзірдегі мәзір элементтері арасында жылжыту.
    • \n' + + '
    • құралдар тақтасының ашылмалы мәзіріндегі мәзір элементтері арасында жылжыту.
    • \n' + + '
    \n' + + '\n' + + '

    Фокусталған ПИ бөліміндегі Arrow (Көрсеткі) пернелерінің циклі.

    \n' + + '\n' + + '

    Ашық мәзірді жабу үшін ішкі мәзірді ашып немесе ашылмалы мәзірді ашып, Esc пернесін басыңыз.

    \n' + + '\n' + + '

    Ағымдағы фокус белгілі бір ПИ бөлімінің «үстінде» болса, Esc пернесін басу әрекеті пернетақта\n' + + ' навигациясын толығымен жабады.

    \n' + + '\n' + + '

    Мәзір элементін немесе құралдар тақтасы түймесін орындау

    \n' + + '\n' + + '

    Қажетті мәзір элементі немесе құралдар тақтасы түймесі бөлектелген кезде, элементті орындау үшін Return (Қайтару), Enter (Енгізу)\n' + + ' немесе Space bar (Бос орын) пернесін басыңыз.

    \n' + + '\n' + + '

    Белгіленбеген диалог терезелерін навигациялау

    \n' + + '\n' + + '

    Белгіленбеген диалог терезелерінде диалог терезесі ашылған кезде бірінші интерактивті құрамдас фокусталады.

    \n' + + '\n' + + '

    Tab немесе Shift+Tab пернесін басу арқылы интерактивті диалог терезесінің құрамдастары арасында навигациялаңыз.

    \n' + + '\n' + + '

    Белгіленген диалог терезелерін навигациялау

    \n' + + '\n' + + '

    Белгіленген диалог терезелерінде диалог терезесі ашылған кезде қойынды мәзіріндегі бірінші түйме фокусталады.

    \n' + + '\n' + + '

    Tab немесе\n' + + ' Shift+Tab пернесін басу арқылы осы диалог терезесі қойындысының интерактивті құрамдастары арасында навигациялаңыз.

    \n' + + '\n' + + '

    Қойынды мәзірінің фокусын беру арқылы басқа диалог терезесінің қойындысына ауысып, тиісті Arrow (Көрсеткі)\n' + + ' пернесін басу арқылы қолжетімді қойындылар арасында айналдыруға болады.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ko_KR.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ko_KR.js new file mode 100644 index 0000000..e7c8e7f --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ko_KR.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.ko_KR', +'

    키보드 탐색 시작

    \n' + + '\n' + + '
    \n' + + '
    메뉴 모음 포커스 표시
    \n' + + '
    Windows 또는 Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    도구 모음 포커스 표시
    \n' + + '
    Windows 또는 Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    푸터 포커스 표시
    \n' + + '
    Windows 또는 Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    알림 포커스
    \n' + + '
    Windows 또는 Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    컨텍스트 도구 모음에 포커스 표시
    \n' + + '
    Windows, Linux 또는 macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    첫 번째 UI 항목에서 탐색이 시작되며, 이때 첫 번째 항목이 강조 표시되거나 푸터 요소 경로에 있는\n' + + ' 경우 밑줄 표시됩니다.

    \n' + + '\n' + + '

    UI 섹션 간 탐색

    \n' + + '\n' + + '

    한 UI 섹션에서 다음 UI 섹션으로 이동하려면 Tab(탭)을 누릅니다.

    \n' + + '\n' + + '

    한 UI 섹션에서 이전 UI 섹션으로 돌아가려면 Shift+Tab(시프트+탭)을 누릅니다.

    \n' + + '\n' + + '

    이 UI 섹션의 Tab(탭) 순서는 다음과 같습니다.

    \n' + + '\n' + + '
      \n' + + '
    1. 메뉴 바
    2. \n' + + '
    3. 각 도구 모음 그룹
    4. \n' + + '
    5. 사이드바
    6. \n' + + '
    7. 푸터의 요소 경로
    8. \n' + + '
    9. 푸터의 단어 수 토글 버튼
    10. \n' + + '
    11. 푸터의 브랜딩 링크
    12. \n' + + '
    13. 푸터의 에디터 크기 변경 핸들
    14. \n' + + '
    \n' + + '\n' + + '

    UI 섹션이 없는 경우 건너뛰기합니다.

    \n' + + '\n' + + '

    푸터에 키보드 탐색 포커스가 있고 사이드바는 보이지 않는 경우 Shift+Tab(시프트+탭)을 누르면\n' + + ' 포커스 표시가 마지막이 아닌 첫 번째 도구 모음 그룹으로 이동합니다.

    \n' + + '\n' + + '

    UI 섹션 내 탐색

    \n' + + '\n' + + '

    한 UI 요소에서 다음 UI 요소로 이동하려면 적절한 화살표 키를 누릅니다.

    \n' + + '\n' + + '

    왼쪽오른쪽 화살표 키의 용도:

    \n' + + '\n' + + '
      \n' + + '
    • 메뉴 모음에서 메뉴 항목 사이를 이동합니다.
    • \n' + + '
    • 메뉴에서 하위 메뉴를 엽니다.
    • \n' + + '
    • 도구 모음 그룹에서 버튼 사이를 이동합니다.
    • \n' + + '
    • 푸터의 요소 경로에서 항목 간에 이동합니다.
    • \n' + + '
    \n' + + '\n' + + '

    아래 화살표 키의 용도:

    \n' + + '\n' + + '
      \n' + + '
    • 메뉴에서 메뉴 항목 사이를 이동합니다.
    • \n' + + '
    • 도구 모음 팝업 메뉴에서 메뉴 항목 사이를 이동합니다.
    • \n' + + '
    \n' + + '\n' + + '

    화살표 키는 포커스 표시 UI 섹션 내에서 순환됩니다.

    \n' + + '\n' + + '

    열려 있는 메뉴, 열려 있는 하위 메뉴 또는 열려 있는 팝업 메뉴를 닫으려면 Esc 키를 누릅니다.

    \n' + + '\n' + + "

    현재 포커스 표시가 특정 UI 섹션 '상단'에 있는 경우 이때도 Esc 키를 누르면\n" + + ' 키보드 탐색이 완전히 종료됩니다.

    \n' + + '\n' + + '

    메뉴 항목 또는 도구 모음 버튼 실행

    \n' + + '\n' + + '

    원하는 메뉴 항목 또는 도구 모음 버튼이 강조 표시되어 있을 때 Return(리턴), Enter(엔터),\n' + + ' 또는 Space bar(스페이스바)를 눌러 해당 항목을 실행합니다.

    \n' + + '\n' + + '

    탭이 없는 대화 탐색

    \n' + + '\n' + + '

    탭이 없는 대화의 경우, 첫 번째 대화형 요소가 포커스 표시된 상태로 대화가 열립니다.

    \n' + + '\n' + + '

    대화형 요소들 사이를 이동할 때는 Tab(탭) 또는 Shift+Tab(시프트+탭)을 누릅니다.

    \n' + + '\n' + + '

    탭이 있는 대화 탐색

    \n' + + '\n' + + '

    탭이 있는 대화의 경우, 탭 메뉴에서 첫 번째 버튼이 포커스 표시된 상태로 대화가 열립니다.

    \n' + + '\n' + + '

    이 대화 탭의 대화형 요소들 사이를 이동할 때는 Tab(탭) 또는\n' + + ' Shift+Tab(시프트+탭)을 누릅니다.

    \n' + + '\n' + + '

    다른 대화 탭으로 이동하려면 탭 메뉴를 포커스 표시한 다음 적절한 화살표\n' + + ' 키를 눌러 사용 가능한 탭들을 지나 원하는 탭으로 이동합니다.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ms.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ms.js new file mode 100644 index 0000000..2c047bb --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ms.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.ms', +'

    Mulakan navigasi papan kekunci

    \n' + + '\n' + + '
    \n' + + '
    Fokus bar Menu
    \n' + + '
    Windows atau Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokus Bar Alat
    \n' + + '
    Windows atau Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokus pengaki
    \n' + + '
    Windows atau Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Tumpu kepada pemberitahuan
    \n' + + '
    Windows atau Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokus bar alat kontekstual
    \n' + + '
    Windows, Linux atau macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigasi akan bermula pada item UI pertama, yang akan diserlahkan atau digaris bawah dalam saiz item pertama dalam\n' + + ' laluan elemen Pengaki.

    \n' + + '\n' + + '

    Navigasi antara bahagian UI

    \n' + + '\n' + + '

    Untuk bergerak dari satu bahagian UI ke yang seterusnya, tekan Tab.

    \n' + + '\n' + + '

    Untuk bergerak dari satu bahagian UI ke yang sebelumnya, tekan Shift+Tab.

    \n' + + '\n' + + '

    Tertib Tab bahagian UI ini ialah:

    \n' + + '\n' + + '
      \n' + + '
    1. Bar menu
    2. \n' + + '
    3. Setiap kumpulan bar alat
    4. \n' + + '
    5. Bar sisi
    6. \n' + + '
    7. Laluan elemen dalam pengaki
    8. \n' + + '
    9. Butang togol kiraan perkataan dalam pengaki
    10. \n' + + '
    11. Pautan penjenamaan dalam pengaki
    12. \n' + + '
    13. Pemegang saiz semula editor dalam pengaki
    14. \n' + + '
    \n' + + '\n' + + '

    Jika bahagian UI tidak wujud, ia dilangkau.

    \n' + + '\n' + + '

    Jika pengaki mempunyai fokus navigasi papan kekunci dan tiada bar sisi kelihatan, menekan Shift+Tab\n' + + ' akan mengalihkan fokus ke kumpulan bar alat pertama, bukannya yang terakhir.

    \n' + + '\n' + + '

    Navigasi dalam bahagian UI

    \n' + + '\n' + + '

    Untuk bergerak dari satu elemen UI ke yang seterusnya, tekan kekunci Anak Panah yang bersesuaian.

    \n' + + '\n' + + '

    Kekunci anak panah Kiri dan Kanan

    \n' + + '\n' + + '
      \n' + + '
    • bergerak antara menu dalam bar menu.
    • \n' + + '
    • membukan submenu dalam menu.
    • \n' + + '
    • bergerak antara butang dalam kumpulan bar alat.
    • \n' + + '
    • Laluan elemen dalam pengaki.
    • \n' + + '
    \n' + + '\n' + + '

    Kekunci anak panah Bawah dan Atas

    \n' + + '\n' + + '
      \n' + + '
    • bergerak antara item menu dalam menu.
    • \n' + + '
    • bergerak antara item dalam menu timbul bar alat.
    • \n' + + '
    \n' + + '\n' + + '

    Kekunci Anak Panah berkitar dalam bahagian UI difokuskan.

    \n' + + '\n' + + '

    Untuk menutup menu buka, submenu terbuka atau menu timbul terbuka, tekan kekunci Esc.

    \n' + + '\n' + + "

    Jika fokus semasa berada di bahagian 'atas' bahagian UI tertentu, menekan kekunci Esc juga akan keluar daripada\n" + + ' navigasi papan kekunci sepenuhnya.

    \n' + + '\n' + + '

    Laksanakan item menu atau butang bar alat

    \n' + + '\n' + + '

    Apabila item menu atau butang bar alat yang diinginkan diserlahkan, tekan Return, Enter,\n' + + ' atau bar Space untuk melaksanakan item.

    \n' + + '\n' + + '

    Navigasi ke dialog tidak bertab

    \n' + + '\n' + + '

    Dalam dialog tidak bertab, komponen interaksi pertama difokuskan apabila dialog dibuka.

    \n' + + '\n' + + '

    Navigasi antara komponen dialog interaktif dengan menekan Tab atau Shift+Tab.

    \n' + + '\n' + + '

    Navigasi ke dialog bertab

    \n' + + '\n' + + '

    Dalam dialog bertab, butang pertama dalam menu tab difokuskan apabila dialog dibuka.

    \n' + + '\n' + + '

    Navigasi antara komponen interaktif tab dialog ini dengan menekan Tab atau\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Tukar kepada tab dialog lain dengan memfokuskan menu tab, kemudian menekan kekunci Anak Panah yang bersesuaian\n' + + ' untuk berkitar menerusi tab yang tersedia.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/nb_NO.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/nb_NO.js new file mode 100644 index 0000000..071e3f5 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/nb_NO.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.nb_NO', +'

    Starte tastaturnavigering

    \n' + + '\n' + + '
    \n' + + '
    Utheve menylinjen
    \n' + + '
    Windows eller Linux: Alt + F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Utheve verktøylinjen
    \n' + + '
    Windows eller Linux: Alt + F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Utheve bunnteksten
    \n' + + '
    Windows eller Linux: Alt + F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Fokuser på varselet
    \n' + + '
    Windows eller Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Utheve en kontekstuell verktøylinje
    \n' + + '
    Windows, Linux eller macOS: Ctrl + F9
    \n' + + '
    \n' + + '\n' + + '

    Navigeringen starter ved det første grensesnittelementet, som utheves, eller understrekes når det gjelder det første elementet i\n' + + ' elementstien i bunnteksten.

    \n' + + '\n' + + '

    Navigere mellom grensesnittdeler

    \n' + + '\n' + + '

    Du kan bevege deg fra én grensesnittdel til den neste ved å trykke på tabulatortasten.

    \n' + + '\n' + + '

    Du kan bevege deg fra én grensesnittdel til den forrige ved å trykke på Shift + tabulatortasten.

    \n' + + '\n' + + '

    Rekkefølgen til tabulatortasten gjennom grensesnittdelene er:

    \n' + + '\n' + + '
      \n' + + '
    1. Menylinjen
    2. \n' + + '
    3. Hver gruppe på verktøylinjen
    4. \n' + + '
    5. Sidestolpen
    6. \n' + + '
    7. Elementstien i bunnteksten
    8. \n' + + '
    9. Veksleknappen for ordantall i bunnteksten
    10. \n' + + '
    11. Merkelenken i bunnteksten
    12. \n' + + '
    13. Skaleringshåndtaket for redigeringsprogrammet i bunnteksten
    14. \n' + + '
    \n' + + '\n' + + '

    Hvis en grensesnittdel ikke er til stede, blir den hoppet over.

    \n' + + '\n' + + '

    Hvis tastaturnavigeringen har uthevet bunnteksten og det ikke finnes en synlig sidestolpe, kan du trykke på Shift + tabulatortasten\n' + + ' for å flytte fokuset til den første gruppen på verktøylinjen i stedet for den siste.

    \n' + + '\n' + + '

    Navigere innenfor grensesnittdeler

    \n' + + '\n' + + '

    Du kan bevege deg fra ett grensesnittelement til det neste ved å trykke på den aktuelle piltasten.

    \n' + + '\n' + + '

    De venstre og høyre piltastene

    \n' + + '\n' + + '
      \n' + + '
    • beveger deg mellom menyer på menylinjen.
    • \n' + + '
    • åpner en undermeny i en meny.
    • \n' + + '
    • beveger deg mellom knapper i en gruppe på verktøylinjen.
    • \n' + + '
    • beveger deg mellom elementer i elementstien i bunnteksten.
    • \n' + + '
    \n' + + '\n' + + '

    Ned- og opp-piltastene

    \n' + + '\n' + + '
      \n' + + '
    • beveger deg mellom menyelementer i en meny.
    • \n' + + '
    • beveger deg mellom elementer i en hurtigmeny på verktøylinjen.
    • \n' + + '
    \n' + + '\n' + + '

    Med piltastene kan du bevege deg innenfor den uthevede grensesnittdelen.

    \n' + + '\n' + + '

    Du kan lukke en åpen meny, en åpen undermeny eller en åpen hurtigmeny ved å klikke på Esc-tasten.

    \n' + + '\n' + + '

    Hvis det øverste nivået i en grensesnittdel er uthevet, kan du ved å trykke på Esc også avslutte\n' + + ' tastaturnavigeringen helt.

    \n' + + '\n' + + '

    Utføre et menyelement eller en knapp på en verktøylinje

    \n' + + '\n' + + '

    Når det ønskede menyelementet eller verktøylinjeknappen er uthevet, trykker du på Retur, Enter,\n' + + ' eller mellomromstasten for å utføre elementet.

    \n' + + '\n' + + '

    Navigere i dialogbokser uten faner

    \n' + + '\n' + + '

    I dialogbokser uten faner blir den første interaktive komponenten uthevet når dialogboksen åpnes.

    \n' + + '\n' + + '

    Naviger mellom interaktive komponenter i dialogboksen ved å trykke på tabulatortasten eller Shift + tabulatortasten.

    \n' + + '\n' + + '

    Navigere i fanebaserte dialogbokser

    \n' + + '\n' + + '

    I fanebaserte dialogbokser blir den første knappen i fanemenyen uthevet når dialogboksen åpnes.

    \n' + + '\n' + + '

    Naviger mellom interaktive komponenter i fanen ved å trykke på tabulatortasten eller\n' + + ' Shift + tabulatortasten.

    \n' + + '\n' + + '

    Veksle til en annen fane i dialogboksen ved å utheve fanemenyen, og trykk deretter på den aktuelle piltasten\n' + + ' for å bevege deg mellom de tilgjengelige fanene.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/nl.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/nl.js new file mode 100644 index 0000000..05c07ae --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/nl.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.nl', +'

    Toetsenbordnavigatie starten

    \n' + + '\n' + + '
    \n' + + '
    Focus op de menubalk instellen
    \n' + + '
    Windows of Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Focus op de werkbalk instellen
    \n' + + '
    Windows of Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Focus op de voettekst instellen
    \n' + + '
    Windows of Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Focus op de melding instellen
    \n' + + '
    Windows of Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Focus op een contextuele werkbalk instellen
    \n' + + '
    Windows, Linux of macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    De navigatie start bij het eerste UI-item, dat wordt gemarkeerd of onderstreept als het eerste item zich in\n' + + ' in het elementenpad van de voettekst bevindt.

    \n' + + '\n' + + '

    Navigeren tussen UI-secties

    \n' + + '\n' + + '

    Druk op Tab om naar de volgende UI-sectie te gaan.

    \n' + + '\n' + + '

    Druk op Shift+Tab om naar de vorige UI-sectie te gaan.

    \n' + + '\n' + + '

    De Tab-volgorde van deze UI-secties is:

    \n' + + '\n' + + '
      \n' + + '
    1. Menubalk
    2. \n' + + '
    3. Elke werkbalkgroep
    4. \n' + + '
    5. Zijbalk
    6. \n' + + '
    7. Elementenpad in de voettekst
    8. \n' + + '
    9. Wisselknop voor aantal woorden in de voettekst
    10. \n' + + '
    11. Merkkoppeling in de voettekst
    12. \n' + + '
    13. Greep voor het wijzigen van het formaat van de editor in de voettekst
    14. \n' + + '
    \n' + + '\n' + + '

    Als een UI-sectie niet aanwezig is, wordt deze overgeslagen.

    \n' + + '\n' + + '

    Als de focus van de toetsenbordnavigatie is ingesteld op de voettekst en er geen zichtbare zijbalk is, kun je op Shift+Tab drukken\n' + + ' om de focus naar de eerste werkbalkgroep in plaats van de laatste te verplaatsen.

    \n' + + '\n' + + '

    Navigeren binnen UI-secties

    \n' + + '\n' + + '

    Druk op de pijltjestoets om naar het betreffende UI-element te gaan.

    \n' + + '\n' + + '

    Met de pijltjestoetsen Links en Rechts

    \n' + + '\n' + + '
      \n' + + "
    • wissel je tussen menu's in de menubalk.
    • \n" + + '
    • open je een submenu in een menu.
    • \n' + + '
    • wissel je tussen knoppen in een werkbalkgroep.
    • \n' + + '
    • wissel je tussen items in het elementenpad in de voettekst.
    • \n' + + '
    \n' + + '\n' + + '

    Met de pijltjestoetsen Omlaag en Omhoog

    \n' + + '\n' + + '
      \n' + + '
    • wissel je tussen menu-items in een menu.
    • \n' + + '
    • wissel je tussen items in een werkbalkpop-upmenu.
    • \n' + + '
    \n' + + '\n' + + '

    Met de pijltjestoetsen wissel je binnen de UI-sectie waarop de focus is ingesteld.

    \n' + + '\n' + + '

    Druk op de toets Esc om een geopend menu, submenu of pop-upmenu te sluiten.

    \n' + + '\n' + + "

    Als de huidige focus is ingesteld 'bovenaan' een bepaalde UI-sectie, kun je op de toets Esc drukken\n" + + ' om de toetsenbordnavigatie af te sluiten.

    \n' + + '\n' + + '

    Een menu-item of werkbalkknop uitvoeren

    \n' + + '\n' + + '

    Als het gewenste menu-item of de gewenste werkbalkknop is gemarkeerd, kun je op Return, Enter\n' + + ' of de spatiebalk drukken om het item uit te voeren.

    \n' + + '\n' + + '

    Navigeren in dialoogvensters zonder tabblad

    \n' + + '\n' + + '

    Als een dialoogvenster zonder tabblad wordt geopend, wordt de focus ingesteld op het eerste interactieve onderdeel.

    \n' + + '\n' + + '

    Je kunt navigeren tussen interactieve onderdelen van een dialoogvenster door op Tab of Shift+Tab te drukken.

    \n' + + '\n' + + '

    Navigeren in dialoogvensters met tabblad

    \n' + + '\n' + + '

    Als een dialoogvenster met tabblad wordt geopend, wordt de focus ingesteld op de eerste knop in het tabbladmenu.

    \n' + + '\n' + + '

    Je kunt navigeren tussen interactieve onderdelen van dit tabblad van het dialoogvenster door op Tab of\n' + + ' Shift+Tab te drukken.

    \n' + + '\n' + + '

    Je kunt overschakelen naar een ander tabblad van het dialoogvenster door de focus in te stellen op het tabbladmenu en vervolgens op de juiste pijltjestoets\n' + + ' te drukken om tussen de beschikbare tabbladen te wisselen.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pl.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pl.js new file mode 100644 index 0000000..e89f808 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pl.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.pl', +'

    Początek nawigacji przy użyciu klawiatury

    \n' + + '\n' + + '
    \n' + + '
    Ustaw fokus na pasek menu
    \n' + + '
    Windows lub Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Ustaw fokus na pasek narzędzi
    \n' + + '
    Windows lub Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Ustaw fokus na sekcję Footer
    \n' + + '
    Windows lub Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Skup się na powiadomieniu
    \n' + + '
    Windows lub Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Ustaw fokus na kontekstowy pasek narzędzi
    \n' + + '
    Windows, Linux lub macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Nawigacja zostanie rozpoczęta od pierwszego elementu interfejsu użytkownika, który jest podświetlony lub — w przypadku pierwszego elementu\n' + + ' w ścieżce elementów w sekcji Footer — podkreślony.

    \n' + + '\n' + + '

    Nawigacja pomiędzy sekcjami interfejsu użytkownika

    \n' + + '\n' + + '

    Aby przenieść się z danej sekcji interfejsu użytkownika do następnej, naciśnij Tab.

    \n' + + '\n' + + '

    Aby przenieść się z danej sekcji interfejsu użytkownika do poprzedniej, naciśnij Shift+Tab.

    \n' + + '\n' + + '

    Kolejność klawisza Tab w takich sekcjach interfejsu użytkownika jest następująca:

    \n' + + '\n' + + '
      \n' + + '
    1. Pasek menu
    2. \n' + + '
    3. Każda grupa na pasku narzędzi
    4. \n' + + '
    5. Pasek boczny
    6. \n' + + '
    7. Ścieżka elementów w sekcji Footer
    8. \n' + + '
    9. Przycisk przełączania liczby słów w sekcji Footer
    10. \n' + + '
    11. Łącze brandujące w sekcji Footer
    12. \n' + + '
    13. Uchwyt zmiany rozmiaru edytora w sekcji Footer
    14. \n' + + '
    \n' + + '\n' + + '

    Jeżeli nie ma sekcji interfejsu użytkownika, jest to pomijane.

    \n' + + '\n' + + '

    Jeżeli na sekcji Footer jest ustawiony fokus nawigacji przy użyciu klawiatury i nie ma widocznego paska bocznego, naciśnięcie Shift+Tab\n' + + ' przenosi fokus na pierwszą grupę paska narzędzi, a nie na ostatnią.

    \n' + + '\n' + + '

    Nawigacja wewnątrz sekcji interfejsu użytkownika

    \n' + + '\n' + + '

    Aby przenieść się z danego elementu interfejsu użytkownika do następnego, naciśnij odpowiedni klawisz strzałki.

    \n' + + '\n' + + '

    Klawisze strzałek w prawo i w lewo służą do

    \n' + + '\n' + + '
      \n' + + '
    • przenoszenia się pomiędzy menu na pasku menu,
    • \n' + + '
    • otwarcia podmenu w menu,
    • \n' + + '
    • przenoszenia się pomiędzy przyciskami w grupie paska narzędzi,
    • \n' + + '
    • przenoszenia się pomiędzy elementami w ścieżce elementów w sekcji Footer.
    • \n' + + '
    \n' + + '\n' + + '

    Klawisze strzałek w dół i w górę służą do

    \n' + + '\n' + + '
      \n' + + '
    • przenoszenia się pomiędzy elementami menu w menu,
    • \n' + + '
    • przenoszenia się pomiędzy elementami w wyskakującym menu paska narzędzi.
    • \n' + + '
    \n' + + '\n' + + '

    Klawisze strzałek służą do przemieszczania się w sekcji interfejsu użytkownika z ustawionym fokusem.

    \n' + + '\n' + + '

    Aby zamknąć otwarte menu, otwarte podmenu lub otwarte menu wyskakujące, naciśnij klawisz Esc.

    \n' + + '\n' + + '

    Jeżeli fokus jest ustawiony na górze konkretnej sekcji interfejsu użytkownika, naciśnięcie klawisza Esc powoduje wyjście\n' + + ' z nawigacji przy użyciu klawiatury.

    \n' + + '\n' + + '

    Wykonanie elementu menu lub przycisku paska narzędzi

    \n' + + '\n' + + '

    Gdy podświetlony jest żądany element menu lub przycisk paska narzędzi, naciśnij klawisz Return, Enter\n' + + ' lub Spacja, aby go wykonać.

    \n' + + '\n' + + '

    Nawigacja po oknie dialogowym bez kart

    \n' + + '\n' + + '

    Gdy otwiera się okno dialogowe bez kart, fokus ustawiany jest na pierwszą interaktywną część okna.

    \n' + + '\n' + + '

    Pomiędzy interaktywnymi częściami okna dialogowego nawiguj, naciskając klawisze Tab lub Shift+Tab.

    \n' + + '\n' + + '

    Nawigacja po oknie dialogowym z kartami

    \n' + + '\n' + + '

    W przypadku okna dialogowego z kartami po otwarciu okna dialogowego fokus ustawiany jest na pierwszy przycisk w menu karty.

    \n' + + '\n' + + '

    Nawigację pomiędzy interaktywnymi częściami karty okna dialogowego prowadzi się poprzez naciskanie klawiszy Tab lub\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Przełączenie się na inną kartę okna dialogowego wykonuje się poprzez ustawienie fokusu na menu karty i naciśnięcie odpowiedniego klawisza strzałki\n' + + ' w celu przemieszczenia się pomiędzy dostępnymi kartami.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pt_BR.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pt_BR.js new file mode 100644 index 0000000..2938fcf --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pt_BR.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.pt_BR', +'

    Iniciar navegação pelo teclado

    \n' + + '\n' + + '
    \n' + + '
    Foco na barra de menus
    \n' + + '
    Windows ou Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Foco na barra de ferramentas
    \n' + + '
    Windows ou Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Foco no rodapé
    \n' + + '
    Windows ou Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Foco na notificação
    \n' + + '
    Windows ou Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Foco na barra de ferramentas contextual
    \n' + + '
    Windows, Linux ou macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    A navegação inicia no primeiro item da IU, que será destacado ou sublinhado no caso do primeiro item no\n' + + ' caminho do elemento Rodapé.

    \n' + + '\n' + + '

    Navegar entre seções da IU

    \n' + + '\n' + + '

    Para ir de uma seção da IU para a seguinte, pressione Tab.

    \n' + + '\n' + + '

    Para ir de uma seção da IU para a anterior, pressione Shift+Tab.

    \n' + + '\n' + + '

    A ordem de Tab destas seções da IU é:

    \n' + + '\n' + + '
      \n' + + '
    1. Barra de menus
    2. \n' + + '
    3. Cada grupo da barra de ferramentas
    4. \n' + + '
    5. Barra lateral
    6. \n' + + '
    7. Caminho do elemento no rodapé
    8. \n' + + '
    9. Botão de alternar contagem de palavras no rodapé
    10. \n' + + '
    11. Link da marca no rodapé
    12. \n' + + '
    13. Alça de redimensionamento do editor no rodapé
    14. \n' + + '
    \n' + + '\n' + + '

    Se não houver uma seção da IU, ela será pulada.

    \n' + + '\n' + + '

    Se o rodapé tiver o foco da navegação pelo teclado e não houver uma barra lateral visível, pressionar Shift+Tab\n' + + ' move o foco para o primeiro grupo da barra de ferramentas, não para o último.

    \n' + + '\n' + + '

    Navegar dentro das seções da IU

    \n' + + '\n' + + '

    Para ir de um elemento da IU para o seguinte, pressione a Seta correspondente.

    \n' + + '\n' + + '

    As teclas de seta Esquerda e Direita

    \n' + + '\n' + + '
      \n' + + '
    • movem entre menus na barra de menus.
    • \n' + + '
    • abrem um submenu em um menu.
    • \n' + + '
    • movem entre botões em um grupo da barra de ferramentas.
    • \n' + + '
    • movem entre itens no caminho do elemento do rodapé.
    • \n' + + '
    \n' + + '\n' + + '

    As teclas de seta Abaixo e Acima

    \n' + + '\n' + + '
      \n' + + '
    • movem entre itens de menu em um menu.
    • \n' + + '
    • movem entre itens em um menu suspenso da barra de ferramentas.
    • \n' + + '
    \n' + + '\n' + + '

    As teclas de Seta alternam dentre a seção da IU em foco.

    \n' + + '\n' + + '

    Para fechar um menu aberto, um submenu aberto ou um menu suspenso aberto, pressione Esc.

    \n' + + '\n' + + '

    Se o foco atual estiver no ‘alto’ de determinada seção da IU, pressionar Esc também sai\n' + + ' totalmente da navegação pelo teclado.

    \n' + + '\n' + + '

    Executar um item de menu ou botão da barra de ferramentas

    \n' + + '\n' + + '

    Com o item de menu ou botão da barra de ferramentas desejado destacado, pressione Return, Enter,\n' + + ' ou a Barra de espaço para executar o item.

    \n' + + '\n' + + '

    Navegar por caixas de diálogo sem guias

    \n' + + '\n' + + '

    Em caixas de diálogo sem guias, o primeiro componente interativo recebe o foco quando a caixa de diálogo abre.

    \n' + + '\n' + + '

    Navegue entre componentes interativos de caixa de diálogo pressionando Tab ou Shift+Tab.

    \n' + + '\n' + + '

    Navegar por caixas de diálogo com guias

    \n' + + '\n' + + '

    Em caixas de diálogo com guias, o primeiro botão no menu da guia recebe o foco quando a caixa de diálogo abre.

    \n' + + '\n' + + '

    Navegue entre componentes interativos dessa guia da caixa de diálogo pressionando Tab ou\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Alterne para outra guia da caixa de diálogo colocando o foco no menu da guia e pressionando a Seta\n' + + ' adequada para percorrer as guias disponíveis.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pt_PT.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pt_PT.js new file mode 100644 index 0000000..03da3d6 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/pt_PT.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.pt_PT', +'

    Iniciar navegação com teclado

    \n' + + '\n' + + '
    \n' + + '
    Foco na barra de menu
    \n' + + '
    Windows ou Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Foco na barra de ferramentas
    \n' + + '
    Windows ou Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Foco no rodapé
    \n' + + '
    Windows ou Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Focar a notificação
    \n' + + '
    Windows ou Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Foco numa barra de ferramentas contextual
    \n' + + '
    Windows, Linux ou macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    A navegação começará no primeiro item de IU, que estará realçado ou sublinhado, no caso do primeiro item no\n' + + ' caminho do elemento do rodapé.

    \n' + + '\n' + + '

    Navegar entre secções de IU

    \n' + + '\n' + + '

    Para se mover de uma secção de IU para a seguinte, prima Tab.

    \n' + + '\n' + + '

    Para se mover de uma secção de IU para a anterior, prima Shift+Tab.

    \n' + + '\n' + + '

    A ordem de tabulação destas secções de IU é:

    \n' + + '\n' + + '
      \n' + + '
    1. Barra de menu
    2. \n' + + '
    3. Cada grupo da barra de ferramentas
    4. \n' + + '
    5. Barra lateral
    6. \n' + + '
    7. Caminho do elemento no rodapé
    8. \n' + + '
    9. Botão de alternar da contagem de palavras no rodapé
    10. \n' + + '
    11. Ligação da marca no rodapé
    12. \n' + + '
    13. Alça de redimensionamento do editor no rodapé
    14. \n' + + '
    \n' + + '\n' + + '

    Se uma secção de IU não estiver presente, é ignorada.

    \n' + + '\n' + + '

    Se o rodapé tiver foco de navegação com teclado e não existir uma barra lateral visível, premir Shift+Tab\n' + + ' move o foco para o primeiro grupo da barra de ferramentas e não para o último.

    \n' + + '\n' + + '

    Navegar nas secções de IU

    \n' + + '\n' + + '

    Para se mover de um elemento de IU para o seguinte, prima a tecla de seta adequada.

    \n' + + '\n' + + '

    As teclas de seta Para a esquerda e Para a direita

    \n' + + '\n' + + '
      \n' + + '
    • movem-se entre menus na barra de menu.
    • \n' + + '
    • abrem um submenu num menu.
    • \n' + + '
    • movem-se entre botões num grupo da barra de ferramentas.
    • \n' + + '
    • movem-se entre itens no caminho do elemento do rodapé.
    • \n' + + '
    \n' + + '\n' + + '

    As teclas de seta Para cima e Para baixo

    \n' + + '\n' + + '
      \n' + + '
    • movem-se entre itens de menu num menu.
    • \n' + + '
    • movem-se entre itens num menu de pop-up da barra de ferramentas.
    • \n' + + '
    \n' + + '\n' + + '

    As teclas de seta deslocam-se ciclicamente na secção de IU em foco.

    \n' + + '\n' + + '

    Para fechar um menu aberto, um submenu aberto ou um menu de pop-up aberto, prima a tecla Esc.

    \n' + + '\n' + + '

    Se o foco atual estiver no "topo" de determinada secção de IU, premir a tecla Esc também fecha\n' + + ' completamente a navegação com teclado.

    \n' + + '\n' + + '

    Executar um item de menu ou botão da barra de ferramentas

    \n' + + '\n' + + '

    Quando o item de menu ou o botão da barra de ferramentas pretendido estiver realçado, prima Retrocesso, Enter\n' + + ' ou a Barra de espaço para executar o item.

    \n' + + '\n' + + '

    Navegar em diálogos sem separadores

    \n' + + '\n' + + '

    Nos diálogos sem separadores, o primeiro componente interativo fica em foco quando o diálogo abre.

    \n' + + '\n' + + '

    Navegue entre componentes interativos do diálogo, premindo Tab ou Shift+Tab.

    \n' + + '\n' + + '

    Navegar em diálogos com separadores

    \n' + + '\n' + + '

    Nos diálogos com separadores, o primeiro botão no menu do separador fica em foco quando o diálogo abre.

    \n' + + '\n' + + '

    Navegue entre os componentes interativos deste separador do diálogo, premindo Tab ou\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Mude para outro separador do diálogo colocando o menu do separador em foco e, em seguida, premindo a tecla de seta\n' + + ' adequada para se deslocar ciclicamente pelos separadores disponíveis.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ro.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ro.js new file mode 100644 index 0000000..38d3441 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ro.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.ro', +'

    Începeți navigarea de la tastatură

    \n' + + '\n' + + '
    \n' + + '
    Focalizare pe bara de meniu
    \n' + + '
    Windows sau Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Focalizare pe bara de instrumente
    \n' + + '
    Windows sau Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Focalizare pe subsol
    \n' + + '
    Windows sau Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Focalizare pe notificare
    \n' + + '
    Windows sau Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Focalizare pe o bară de instrumente contextuală
    \n' + + '
    Windows, Linux sau macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigarea va începe de la primul element al interfeței cu utilizatorul, care va fi evidențiat sau subliniat în cazul primului element din\n' + + ' calea elementului Subsol.

    \n' + + '\n' + + '

    Navigați între secțiunile interfeței cu utilizatorul

    \n' + + '\n' + + '

    Pentru a trece de la o secțiune a interfeței cu utilizatorul la alta, apăsați Tab.

    \n' + + '\n' + + '

    Pentru a trece de la o secțiune a interfeței cu utilizatorul la cea anterioară, apăsați Shift+Tab.

    \n' + + '\n' + + '

    Ordinea cu Tab a acestor secțiuni ale interfeței cu utilizatorul este următoarea:

    \n' + + '\n' + + '
      \n' + + '
    1. Bara de meniu
    2. \n' + + '
    3. Fiecare grup de bare de instrumente
    4. \n' + + '
    5. Bara laterală
    6. \n' + + '
    7. Calea elementului în subsol
    8. \n' + + '
    9. Buton de comutare a numărului de cuvinte în subsol
    10. \n' + + '
    11. Link de branding în subsol
    12. \n' + + '
    13. Mâner de redimensionare a editorului în subsol
    14. \n' + + '
    \n' + + '\n' + + '

    În cazul în care o secțiune a interfeței cu utilizatorul nu este prezentă, aceasta este omisă.

    \n' + + '\n' + + '

    În cazul în care subsolul are focalizarea navigației asupra tastaturii și nu există o bară laterală vizibilă, apăsarea butonului Shift+Tab\n' + + ' mută focalizarea pe primul grup de bare de instrumente, nu pe ultimul.

    \n' + + '\n' + + '

    Navigați în secțiunile interfeței cu utilizatorul

    \n' + + '\n' + + '

    Pentru a trece de la un element de interfață cu utilizatorul la următorul, apăsați tasta cu săgeata corespunzătoare.

    \n' + + '\n' + + '

    Tastele cu săgeți către stânga și dreapta

    \n' + + '\n' + + '
      \n' + + '
    • navighează între meniurile din bara de meniuri.
    • \n' + + '
    • deschid un sub-meniu dintr-un meniu.
    • \n' + + '
    • navighează între butoanele dintr-un grup de bare de instrumente.
    • \n' + + '
    • navighează între elementele din calea elementelor subsolului.
    • \n' + + '
    \n' + + '\n' + + '

    Tastele cu săgeți în sus și în jos

    \n' + + '\n' + + '
      \n' + + '
    • navighează între elementele de meniu dintr-un meniu.
    • \n' + + '
    • navighează între elementele unui meniu pop-up din bara de instrumente.
    • \n' + + '
    \n' + + '\n' + + '

    Tastele cu săgeți navighează în cadrul secțiunii interfeței cu utilizatorul asupra căreia se focalizează.

    \n' + + '\n' + + '

    Pentru a închide un meniu deschis, un sub-meniu deschis sau un meniu pop-up deschis, apăsați tasta Esc.

    \n' + + '\n' + + '

    Dacă focalizarea curentă este asupra „părții superioare” a unei anumite secțiuni a interfeței cu utilizatorul, prin apăsarea tastei Esc se iese, de asemenea,\n' + + ' în întregime din navigarea de la tastatură.

    \n' + + '\n' + + '

    Executarea unui element de meniu sau a unui buton din bara de instrumente

    \n' + + '\n' + + '

    Atunci când elementul de meniu dorit sau butonul dorit din bara de instrumente este evidențiat, apăsați Return, Enter,\n' + + ' sau bara de spațiu pentru a executa elementul.

    \n' + + '\n' + + '

    Navigarea de dialoguri fără file

    \n' + + '\n' + + '

    În dialogurile fără file, prima componentă interactivă beneficiază de focalizare la deschiderea dialogului.

    \n' + + '\n' + + '

    Navigați între componentele dialogului interactiv apăsând Tab sau Shift+Tab.

    \n' + + '\n' + + '

    Navigarea de dialoguri cu file

    \n' + + '\n' + + '

    În dialogurile cu file, primul buton din meniul cu file beneficiază de focalizare la deschiderea dialogului.

    \n' + + '\n' + + '

    Navigați între componentele interactive ale acestei file de dialog apăsând Tab sau\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Treceți la o altă filă de dialog focalizând asupra meniului cu file și apoi apăsând săgeata corespunzătoare\n' + + ' pentru a parcurge filele disponibile.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ru.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ru.js new file mode 100644 index 0000000..d310f54 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/ru.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.ru', +'

    Начните управление с помощью клавиатуры

    \n' + + '\n' + + '
    \n' + + '
    Фокус на панели меню
    \n' + + '
    Windows или Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Фокус на панели инструментов
    \n' + + '
    Windows или Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Фокус на нижнем колонтитуле
    \n' + + '
    Windows или Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Фокус на уведомлении
    \n' + + '
    Windows или Linux: Alt+12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Фокус на контекстной панели инструментов
    \n' + + '
    Windows, Linux или macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Первый доступный для управления элемент интерфейса будет выделен цветом или подчеркнут (если он находится\n' + + ' в пути элементов нижнего колонтитула).

    \n' + + '\n' + + '

    Переход между разделами пользовательского интерфейса

    \n' + + '\n' + + '

    Чтобы перейти из текущего раздела интерфейса в следующий, нажмите Tab.

    \n' + + '\n' + + '

    Чтобы перейти из текущего раздела интерфейса в предыдущий, нажмите Shift+Tab.

    \n' + + '\n' + + '

    Вкладки разделов интерфейса расположены в следующем порядке:

    \n' + + '\n' + + '
      \n' + + '
    1. Панель меню
    2. \n' + + '
    3. Группы панели инструментов
    4. \n' + + '
    5. Боковая панель
    6. \n' + + '
    7. Путь элементов нижнего колонтитула
    8. \n' + + '
    9. Подсчет слов/символов в нижнем колонтитуле
    10. \n' + + '
    11. Брендовая ссылка в нижнем колонтитуле
    12. \n' + + '
    13. Угол для изменения размера окна редактора
    14. \n' + + '
    \n' + + '\n' + + '

    Если раздел интерфейса отсутствует, он пропускается.

    \n' + + '\n' + + '

    Если при управлении с клавиатуры фокус находится на нижнем колонтитуле, а видимая боковая панель отсутствует, то при нажатии сочетания клавиш Shift+Tab\n' + + ' фокус переносится на первую группу панели инструментов, а не на последнюю.

    \n' + + '\n' + + '

    Переход между элементами внутри разделов пользовательского интерфейса

    \n' + + '\n' + + '

    Чтобы перейти от текущего элемента интерфейса к следующему, нажмите соответствующую клавишу со стрелкой.

    \n' + + '\n' + + '

    Клавиши со стрелками влево и вправо позволяют

    \n' + + '\n' + + '
      \n' + + '
    • перемещаться между разными меню в панели меню.
    • \n' + + '
    • открывать разделы меню.
    • \n' + + '
    • перемещаться между кнопками в группе панели инструментов.
    • \n' + + '
    • перемещаться между элементами в пути элементов нижнего колонтитула.
    • \n' + + '
    \n' + + '\n' + + '

    Клавиши со стрелками вниз и вверх позволяют

    \n' + + '\n' + + '
      \n' + + '
    • перемещаться между элементами одного меню.
    • \n' + + '
    • перемещаться между элементами всплывающего меню в панели инструментов.
    • \n' + + '
    \n' + + '\n' + + '

    При использовании клавиш со стрелками вы будете циклически перемещаться по элементам в пределах выбранного раздела интерфейса.

    \n' + + '\n' + + '

    Чтобы закрыть открытое меню, его раздел или всплывающее меню, нажмите клавишу Esc.

    \n' + + '\n' + + '

    Если фокус находится наверху какого-либо раздела интерфейса, нажатие клавиши Esc также приведет\n' + + ' к выходу из режима управления с помощью клавиатуры.

    \n' + + '\n' + + '

    Использование элемента меню или кнопки на панели инструментов

    \n' + + '\n' + + '

    Когда элемент меню или кнопка панели инструментов будут выделены, нажмите Return, Enter\n' + + ' или Space, чтобы их активировать.

    \n' + + '\n' + + '

    Управление в диалоговом окне без вкладок

    \n' + + '\n' + + '

    При открытии диалогового окна без вкладок фокус переносится на первый интерактивный компонент.

    \n' + + '\n' + + '

    Для перехода между интерактивными компонентами диалогового окна нажимайте Tab или Shift+Tab.

    \n' + + '\n' + + '

    Управление в диалоговом окне с вкладками

    \n' + + '\n' + + '

    При открытии диалогового окна с вкладками фокус переносится на первую кнопку в меню вкладок.

    \n' + + '\n' + + '

    Для перехода между интерактивными компонентами этой вкладки диалогового окна нажимайте Tab или\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Для перехода на другую вкладку диалогового окна переместите фокус на меню вкладок, а затем используйте клавиши со стрелками\n' + + ' для циклического переключения между доступными вкладками.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sk.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sk.js new file mode 100644 index 0000000..60cc628 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sk.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.sk', +'

    Začíname s navigáciou pomocou klávesnice

    \n' + + '\n' + + '
    \n' + + '
    Prejsť na panel s ponukami
    \n' + + '
    Windows alebo Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Prejsť na panel nástrojov
    \n' + + '
    Windows alebo Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Prejsť na pätičku
    \n' + + '
    Windows alebo Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Zaostriť na oznámenie
    \n' + + '
    Windows alebo Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Prejsť na kontextový panel nástrojov
    \n' + + '
    Windows, Linux alebo macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigácia začne pri prvej položke používateľského rozhrania, ktorá bude zvýraznená alebo v prípade prvej položky\n' + + ' cesty k pätičke podčiarknutá.

    \n' + + '\n' + + '

    Navigácia medzi časťami používateľského rozhrania

    \n' + + '\n' + + '

    Ak sa chcete posunúť z jednej časti používateľského rozhrania do druhej, stlačte tlačidlo Tab.

    \n' + + '\n' + + '

    Ak sa chcete posunúť z jednej časti používateľského rozhrania do predchádzajúcej, stlačte tlačidlá Shift + Tab.

    \n' + + '\n' + + '

    Poradie prepínania medzi týmito časťami používateľského rozhrania pri stláčaní tlačidla Tab:

    \n' + + '\n' + + '
      \n' + + '
    1. Panel s ponukou
    2. \n' + + '
    3. Každá skupina panela nástrojov
    4. \n' + + '
    5. Bočný panel
    6. \n' + + '
    7. Cesta k prvku v pätičke
    8. \n' + + '
    9. Prepínač počtu slov v pätičke
    10. \n' + + '
    11. Odkaz na informácie o značke v pätičke
    12. \n' + + '
    13. Úchyt na zmenu veľkosti editora v pätičke
    14. \n' + + '
    \n' + + '\n' + + '

    Ak nejaká časť používateľského rozhrania nie je prítomná, preskočí sa.

    \n' + + '\n' + + '

    Ak je pätička vybratá na navigáciu pomocou klávesnice a nie je viditeľný bočný panel, stlačením klávesov Shift+Tab\n' + + ' prejdete na prvú skupinu panela nástrojov, nie na poslednú.

    \n' + + '\n' + + '

    Navigácia v rámci častí používateľského rozhrania

    \n' + + '\n' + + '

    Ak sa chcete posunúť z jedného prvku používateľského rozhrania na ďalší, stlačte príslušný kláves so šípkou.

    \n' + + '\n' + + '

    Klávesy so šípkami doľava a doprava

    \n' + + '\n' + + '
      \n' + + '
    • umožňujú presun medzi ponukami na paneli ponúk,
    • \n' + + '
    • otvárajú podponuku v rámci ponuky,
    • \n' + + '
    • umožňujú presun medzi tlačidlami v skupine panelov nástrojov,
    • \n' + + '
    • umožňujú presun medzi položkami cesty prvku v pätičke.
    • \n' + + '
    \n' + + '\n' + + '

    Klávesy so šípkami dole a hore

    \n' + + '\n' + + '
      \n' + + '
    • umožňujú presun medzi položkami ponuky,
    • \n' + + '
    • umožňujú presun medzi položkami v kontextovej ponuke panela nástrojov.
    • \n' + + '
    \n' + + '\n' + + '

    Klávesy so šípkami vykonávajú prepínanie v rámci vybranej časti používateľského rozhrania.

    \n' + + '\n' + + '

    Ak chcete zatvoriť otvorenú ponuku, otvorenú podponuku alebo otvorenú kontextovú ponuku, stlačte kláves Esc.

    \n' + + '\n' + + '

    Ak je aktuálne vybratá horná časť konkrétneho používateľského rozhrania, stlačením klávesu Esc úplne ukončíte tiež\n' + + ' navigáciu pomocou klávesnice.

    \n' + + '\n' + + '

    Vykonanie príkazu položky ponuky alebo tlačidla panela nástrojov

    \n' + + '\n' + + '

    Keď je zvýraznená požadovaná položka ponuky alebo tlačidlo panela nástrojov, stlačením klávesov Return, Enter\n' + + ' alebo medzerníka vykonáte príslušný príkaz položky.

    \n' + + '\n' + + '

    Navigácia v dialógových oknách bez záložiek

    \n' + + '\n' + + '

    Pri otvorení dialógových okien bez záložiek prejdete na prvý interaktívny komponent.

    \n' + + '\n' + + '

    Medzi interaktívnymi dialógovými komponentmi môžete prechádzať stlačením klávesov Tab alebo Shift+Tab.

    \n' + + '\n' + + '

    Navigácia v dialógových oknách so záložkami

    \n' + + '\n' + + '

    Pri otvorení dialógových okien so záložkami prejdete na prvé tlačidlo v ponuke záložiek.

    \n' + + '\n' + + '

    Medzi interaktívnymi komponentmi tejto dialógovej záložky môžete prechádzať stlačením klávesov Tab alebo\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Ak chcete prepnúť na ďalšiu záložku dialógového okna, prejdite do ponuky záložiek a potom môžete stlačením príslušného klávesu so šípkou\n' + + ' prepínať medzi dostupnými záložkami.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sl_SI.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sl_SI.js new file mode 100644 index 0000000..2b25f5a --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sl_SI.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.sl_SI', +'

    Začetek krmarjenja s tipkovnico

    \n' + + '\n' + + '
    \n' + + '
    Fokus na menijsko vrstico
    \n' + + '
    Windows ali Linux: Alt + F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokus na orodno vrstico
    \n' + + '
    Windows ali Linux: Alt + F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokus na nogo
    \n' + + '
    Windows ali Linux: Alt + F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Označitev obvestila
    \n' + + '
    Windows ali Linux: Alt + F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokus na kontekstualno orodno vrstico
    \n' + + '
    Windows, Linux ali macOS: Ctrl + F9
    \n' + + '
    \n' + + '\n' + + '

    Krmarjenje se bo začelo s prvim elementom uporabniškega vmesnika, ki bo izpostavljena ali podčrtan, če gre za prvi element na\n' + + ' poti do elementa noge.

    \n' + + '\n' + + '

    Krmarjenje med razdelki uporabniškega vmesnika

    \n' + + '\n' + + '

    Če se želite pomakniti z enega dela uporabniškega vmesnika na naslednjega, pritisnite tabulatorko.

    \n' + + '\n' + + '

    Če se želite pomakniti z enega dela uporabniškega vmesnika na prejšnjega, pritisnite shift + tabulatorko.

    \n' + + '\n' + + '

    Zaporedje teh razdelkov uporabniškega vmesnika, ko pritiskate tabulatorko, je:

    \n' + + '\n' + + '
      \n' + + '
    1. Menijska vrstica
    2. \n' + + '
    3. Posamezne skupine orodne vrstice
    4. \n' + + '
    5. Stranska vrstica
    6. \n' + + '
    7. Pod do elementa v nogi
    8. \n' + + '
    9. Gumb za preklop štetja besed v nogi
    10. \n' + + '
    11. Povezava do blagovne znamke v nogi
    12. \n' + + '
    13. Ročaj za spreminjanje velikosti urejevalnika v nogi
    14. \n' + + '
    \n' + + '\n' + + '

    Če razdelek uporabniškega vmesnika ni prisoten, je preskočen.

    \n' + + '\n' + + '

    Če ima noga fokus za krmarjenje s tipkovnico in ni vidne stranske vrstice, s pritiskom na shift + tabulatorko\n' + + ' fokus premaknete na prvo skupino orodne vrstice, ne zadnjo.

    \n' + + '\n' + + '

    Krmarjenje v razdelkih uporabniškega vmesnika

    \n' + + '\n' + + '

    Če se želite premakniti z enega elementa uporabniškega vmesnika na naslednjega, pritisnite ustrezno puščično tipko.

    \n' + + '\n' + + '

    Leva in desna puščična tipka

    \n' + + '\n' + + '
      \n' + + '
    • omogočata premikanje med meniji v menijski vrstici.
    • \n' + + '
    • odpreta podmeni v meniju.
    • \n' + + '
    • omogočata premikanje med gumbi v skupini orodne vrstice.
    • \n' + + '
    • omogočata premikanje med elementi na poti do elementov noge.
    • \n' + + '
    \n' + + '\n' + + '

    Spodnja in zgornja puščična tipka

    \n' + + '\n' + + '
      \n' + + '
    • omogočata premikanje med elementi menija.
    • \n' + + '
    • omogočata premikanje med elementi v pojavnem meniju orodne vrstice.
    • \n' + + '
    \n' + + '\n' + + '

    Puščične tipke omogočajo kroženje znotraj razdelka uporabniškega vmesnika, na katerem je fokus.

    \n' + + '\n' + + '

    Če želite zapreti odprt meni, podmeni ali pojavni meni, pritisnite tipko Esc.

    \n' + + '\n' + + '

    Če je trenutni fokus na »vrhu« določenega razdelka uporabniškega vmesnika, s pritiskom tipke Esc zaprete\n' + + ' tudi celotno krmarjenje s tipkovnico.

    \n' + + '\n' + + '

    Izvajanje menijskega elementa ali gumba orodne vrstice

    \n' + + '\n' + + '

    Ko je označen želeni menijski element ali orodja vrstica, pritisnite vračalko, Enter\n' + + ' ali preslednico, da izvedete element.

    \n' + + '\n' + + '

    Krmarjenje po pogovornih oknih brez zavihkov

    \n' + + '\n' + + '

    Ko odprete pogovorno okno brez zavihkov, ima fokus prva interaktivna komponenta.

    \n' + + '\n' + + '

    Med interaktivnimi komponentami pogovornega okna se premikate s pritiskom tabulatorke ali kombinacije tipke shift + tabulatorke.

    \n' + + '\n' + + '

    Krmarjenje po pogovornih oknih z zavihki

    \n' + + '\n' + + '

    Ko odprete pogovorno okno z zavihki, ima fokus prvi gumb v meniju zavihka.

    \n' + + '\n' + + '

    Med interaktivnimi komponentami tega zavihka pogovornega okna se premikate s pritiskom tabulatorke ali\n' + + ' kombinacije tipke shift + tabulatorke.

    \n' + + '\n' + + '

    Na drug zavihek pogovornega okna preklopite tako, da fokus prestavite na meni zavihka in nato pritisnete ustrezno puščično\n' + + ' tipko, da se pomaknete med razpoložljivimi zavihki.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sv_SE.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sv_SE.js new file mode 100644 index 0000000..c30f2f2 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/sv_SE.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.sv_SE', +'

    Påbörja tangentbordsnavigering

    \n' + + '\n' + + '
    \n' + + '
    Fokusera på menyraden
    \n' + + '
    Windows eller Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Fokusera på verktygsraden
    \n' + + '
    Windows eller Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Fokusera på verktygsraden
    \n' + + '
    Windows eller Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Fokusera aviseringen
    \n' + + '
    Windows eller Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Fokusera på en snabbverktygsrad
    \n' + + '
    Windows, Linux eller macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Navigeringen börjar vid det första gränssnittsobjektet, vilket är markerat eller understruket om det gäller det första objektet i\n' + + ' sidfotens elementsökväg.

    \n' + + '\n' + + '

    Navigera mellan UI-avsnitt

    \n' + + '\n' + + '

    Flytta från ett UI-avsnitt till nästa genom att trycka på Tabb.

    \n' + + '\n' + + '

    Flytta från ett UI-avsnitt till det föregående genom att trycka på Skift+Tabb.

    \n' + + '\n' + + '

    Tabb-ordningen för dessa UI-avsnitt är:

    \n' + + '\n' + + '
      \n' + + '
    1. Menyrad
    2. \n' + + '
    3. Varje verktygsradsgrupp
    4. \n' + + '
    5. Sidoruta
    6. \n' + + '
    7. Elementsökväg i sidfoten
    8. \n' + + '
    9. Växlingsknapp för ordantal i sidfoten
    10. \n' + + '
    11. Varumärkeslänk i sidfoten
    12. \n' + + '
    13. Storlekshandtag för redigeraren i sidfoten
    14. \n' + + '
    \n' + + '\n' + + '

    Om ett UI-avsnitt inte finns hoppas det över.

    \n' + + '\n' + + '

    Om sidfoten har fokus på tangentbordsnavigering, och det inte finns någon synlig sidoruta, flyttas fokus till den första verktygsradsgruppen\n' + + ' när du trycker på Skift+Tabb, inte till den sista.

    \n' + + '\n' + + '

    Navigera i UI-avsnitt

    \n' + + '\n' + + '

    Flytta från ett UI-element till nästa genom att trycka på motsvarande piltangent.

    \n' + + '\n' + + '

    Vänsterpil och högerpil

    \n' + + '\n' + + '
      \n' + + '
    • flytta mellan menyer på menyraden.
    • \n' + + '
    • öppna en undermeny på en meny.
    • \n' + + '
    • flytta mellan knappar i en verktygsradgrupp.
    • \n' + + '
    • flytta mellan objekt i sidfotens elementsökväg.
    • \n' + + '
    \n' + + '\n' + + '

    Nedpil och uppil

    \n' + + '\n' + + '
      \n' + + '
    • flytta mellan menyalternativ på en meny.
    • \n' + + '
    • flytta mellan alternativ på en popup-meny på verktygsraden.
    • \n' + + '
    \n' + + '\n' + + '

    Piltangenterna cirkulerar inom det fokuserade UI-avsnittet.

    \n' + + '\n' + + '

    Tryck på Esc-tangenten om du vill stänga en öppen meny, undermeny eller popup-meny.

    \n' + + '\n' + + '

    Om det aktuella fokuset är högst upp i ett UI-avsnitt avlutas även tangentbordsnavigeringen helt när\n' + + ' du trycker på Esc-tangenten.

    \n' + + '\n' + + '

    Köra ett menyalternativ eller en verktygfältsknapp

    \n' + + '\n' + + '

    När menyalternativet eller verktygsradsknappen är markerad trycker du på Retur, Enter\n' + + ' eller blanksteg för att köra alternativet.

    \n' + + '\n' + + '

    Navigera i dialogrutor utan flikar

    \n' + + '\n' + + '

    I dialogrutor utan flikar är den första interaktiva komponenten i fokus när dialogrutan öppnas.

    \n' + + '\n' + + '

    Navigera mellan interaktiva dialogkomponenter genom att trycka på Tabb eller Skift+Tabb.

    \n' + + '\n' + + '

    Navigera i dialogrutor med flikar

    \n' + + '\n' + + '

    I dialogrutor utan flikar är den första knappen på flikmenyn i fokus när dialogrutan öppnas.

    \n' + + '\n' + + '

    Navigera mellan interaktiva komponenter på dialogrutefliken genom att trycka på Tabb eller\n' + + ' Skift+Tabb.

    \n' + + '\n' + + '

    Växla till en annan dialogruta genom att fokusera på flikmenyn och sedan trycka på motsvarande piltangent\n' + + ' för att cirkulera mellan de tillgängliga flikarna.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/th_TH.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/th_TH.js new file mode 100644 index 0000000..562fe7a --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/th_TH.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.th_TH', +'

    เริ่มต้นการนำทางด้วยแป้นพิมพ์

    \n' + + '\n' + + '
    \n' + + '
    โฟกัสที่แถบเมนู
    \n' + + '
    Windows หรือ Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    โฟกัสที่แถบเครื่องมือ
    \n' + + '
    Windows หรือ Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    โฟกัสที่ส่วนท้าย
    \n' + + '
    Windows หรือ Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    โฟกัสไปที่การแจ้งเตือน
    \n' + + '
    Windows หรือ Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    โฟกัสที่แถบเครื่องมือตามบริบท
    \n' + + '
    Windows, Linux หรือ macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    การนำทางจะเริ่มที่รายการ UI แรก ซึ่งจะมีการไฮไลต์หรือขีดเส้นใต้ไว้ในกรณีที่รายการแรกอยู่ใน\n' + + ' พาธองค์ประกอบส่วนท้าย

    \n' + + '\n' + + '

    การนำทางระหว่างส่วนต่างๆ ของ UI

    \n' + + '\n' + + '

    ในการย้ายจากส่วน UI หนึ่งไปยังส่วนถัดไป ให้กด Tab

    \n' + + '\n' + + '

    ในการย้ายจากส่วน UI หนึ่งไปยังส่วนก่อนหน้า ให้กด Shift+Tab

    \n' + + '\n' + + '

    ลำดับแท็บของส่วนต่างๆ ของ UI คือ:

    \n' + + '\n' + + '
      \n' + + '
    1. แถบเมนู
    2. \n' + + '
    3. แต่ละกลุ่มแถบเครื่องมือ
    4. \n' + + '
    5. แถบข้าง
    6. \n' + + '
    7. พาธองค์ประกอบในส่วนท้าย
    8. \n' + + '
    9. ปุ่มสลับเปิด/ปิดจำนวนคำในส่วนท้าย
    10. \n' + + '
    11. ลิงก์ชื่อแบรนด์ในส่วนท้าย
    12. \n' + + '
    13. จุดจับปรับขนาดของตัวแก้ไขในส่วนท้าย
    14. \n' + + '
    \n' + + '\n' + + '

    หากส่วน UI ไม่ปรากฏ แสดงว่าถูกข้ามไป

    \n' + + '\n' + + '

    หากส่วนท้ายมีการโฟกัสการนำทางแป้นพิมพ์และไม่มีแถบข้างปรากฏ การกด Shift+Tab\n' + + ' จะย้ายการโฟกัสไปที่กลุ่มแถบเครื่องมือแรก ไม่ใช่สุดท้าย

    \n' + + '\n' + + '

    การนำทางภายในส่วนต่างๆ ของ UI

    \n' + + '\n' + + '

    ในการย้ายจากองค์ประกอบ UI หนึ่งไปยังองค์ประกอบส่วนถัดไป ให้กดปุ่มลูกศรที่เหมาะสม

    \n' + + '\n' + + '

    ปุ่มลูกศรซ้ายและขวา

    \n' + + '\n' + + '
      \n' + + '
    • ย้ายไปมาระหว่างเมนูต่างๆ ในแถบเมนู
    • \n' + + '
    • เปิดเมนูย่อยในเมนู
    • \n' + + '
    • ย้ายไปมาระหว่างปุ่มต่างๆ ในกลุ่มแถบเครื่องมือ
    • \n' + + '
    • ย้ายไปมาระหว่างรายการต่างๆ ในพาธองค์ประกอบของส่วนท้าย
    • \n' + + '
    \n' + + '\n' + + '

    ปุ่มลูกศรลงและขึ้น

    \n' + + '\n' + + '
      \n' + + '
    • ย้ายไปมาระหว่างรายการเมนูต่างๆ ในเมนู
    • \n' + + '
    • ย้ายไปมาระหว่างรายการต่างๆ ในเมนูป๊อบอัพแถบเครื่องมือ
    • \n' + + '
    \n' + + '\n' + + '

    ปุ่มลูกศรจะเลื่อนไปมาภายในส่วน UI ที่โฟกัส

    \n' + + '\n' + + '

    ในการปิดเมนูที่เปิดอยู่ เมนูย่อยที่เปิดอยู่ หรือเมนูป๊อบอัพที่เปิดอยู่ ให้กดปุ่ม Esc

    \n' + + '\n' + + '

    หากโฟกัสปัจจุบันอยู่ที่ ‘ด้านบนสุด’ ของส่วน UI เฉพาะ การกดปุ่ม Esc จะทำให้ออกจาก\n' + + ' การนำทางด้วยแป้นพิมพ์ทั้งหมดเช่นกัน

    \n' + + '\n' + + '

    การดำเนินการรายการเมนูหรือปุ่มในแถบเครื่องมือ

    \n' + + '\n' + + '

    เมื่อไฮไลต์รายการเมนูหรือปุ่มในแถบเครื่องมือที่ต้องการ ให้กด Return, Enter\n' + + ' หรือ Space bar เพื่อดำเนินการรายการดังกล่าว

    \n' + + '\n' + + '

    การนำทางสำหรับกล่องโต้ตอบที่ไม่อยู่ในแท็บ

    \n' + + '\n' + + '

    ในกล่องโต้ตอบที่ไม่อยู่ในแท็บ จะโฟกัสที่ส่วนประกอบเชิงโต้ตอบแรกเมื่อกล่องโต้ตอบเปิด

    \n' + + '\n' + + '

    นำทางระหว่างส่วนประกอบเชิงโต้ตอบต่างๆ ของกล่องโต้ตอบ โดยการกด Tab หรือ Shift+Tab

    \n' + + '\n' + + '

    การนำทางสำหรับกล่องโต้ตอบที่อยู่ในแท็บ

    \n' + + '\n' + + '

    ในกล่องโต้ตอบที่อยู่ในแท็บ จะโฟกัสที่ปุ่มแรกในเมนูแท็บเมื่อกล่องโต้ตอบเปิด

    \n' + + '\n' + + '

    นำทางระหว่างส่วนประกอบเชิงโต้ตอบต่างๆ ของแท็บกล่องโต้ตอบนี้โดยการกด Tab หรือ\n' + + ' Shift+Tab

    \n' + + '\n' + + '

    สลับไปยังแท็บกล่องโต้ตอบอื่นโดยการเลือกโฟกัสที่เมนูแท็บ แล้วกดปุ่มลูกศรที่เหมาะสม\n' + + ' เพื่อเลือกแท็บที่ใช้ได้

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/tr.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/tr.js new file mode 100644 index 0000000..37f39b0 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/tr.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.tr', +'

    Klavyeyle gezintiyi başlatma

    \n' + + '\n' + + '
    \n' + + '
    Menü çubuğuna odaklan
    \n' + + '
    Windows veya Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Araç çubuğuna odaklan
    \n' + + '
    Windows veya Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Alt bilgiye odaklan
    \n' + + '
    Windows veya Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Bildirime odakla
    \n' + + '
    Windows veya Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Bağlamsal araç çubuğuna odaklan
    \n' + + '
    Windows, Linux veya macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Gezinti ilk kullanıcı arabirimi öğesinden başlar, bu öğe vurgulanır ya da ilk öğe, Alt bilgi elemanı\n' + + ' yolundaysa altı çizilir.

    \n' + + '\n' + + '

    Kullanıcı arabirimi bölümleri arasında gezinme

    \n' + + '\n' + + '

    Sonraki kullanıcı arabirimi bölümüne gitmek için Sekme tuşuna basın.

    \n' + + '\n' + + '

    Önceki kullanıcı arabirimi bölümüne gitmek için Shift+Sekme tuşlarına basın.

    \n' + + '\n' + + '

    Bu kullanıcı arabirimi bölümlerinin Sekme sırası:

    \n' + + '\n' + + '
      \n' + + '
    1. Menü çubuğu
    2. \n' + + '
    3. Her araç çubuğu grubu
    4. \n' + + '
    5. Kenar çubuğu
    6. \n' + + '
    7. Alt bilgide öğe yolu
    8. \n' + + '
    9. Alt bilgide sözcük sayısı geçiş düğmesi
    10. \n' + + '
    11. Alt bilgide marka bağlantısı
    12. \n' + + '
    13. Alt bilgide düzenleyiciyi yeniden boyutlandırma tutamacı
    14. \n' + + '
    \n' + + '\n' + + '

    Kullanıcı arabirimi bölümü yoksa atlanır.

    \n' + + '\n' + + '

    Alt bilgide klavyeyle gezinti odağı yoksa ve görünür bir kenar çubuğu mevcut değilse Shift+Sekme tuşlarına basıldığında\n' + + ' odak son araç çubuğu yerine ilk araç çubuğu grubuna taşınır.

    \n' + + '\n' + + '

    Kullanıcı arabirimi bölümleri içinde gezinme

    \n' + + '\n' + + '

    Sonraki kullanıcı arabirimi elemanına gitmek için uygun Ok tuşuna basın.

    \n' + + '\n' + + '

    Sol ve Sağ ok tuşları

    \n' + + '\n' + + '
      \n' + + '
    • menü çubuğundaki menüler arasında hareket eder.
    • \n' + + '
    • menüde bir alt menü açar.
    • \n' + + '
    • araç çubuğu grubundaki düğmeler arasında hareket eder.
    • \n' + + '
    • alt bilginin öğe yolundaki öğeler arasında hareket eder.
    • \n' + + '
    \n' + + '\n' + + '

    Aşağı ve Yukarı ok tuşları

    \n' + + '\n' + + '
      \n' + + '
    • menüdeki menü öğeleri arasında hareket eder.
    • \n' + + '
    • araç çubuğu açılır menüsündeki öğeler arasında hareket eder.
    • \n' + + '
    \n' + + '\n' + + '

    Ok tuşları, odaklanılan kullanıcı arabirimi bölümü içinde döngüsel olarak hareket eder.

    \n' + + '\n' + + '

    Açık bir menüyü, açık bir alt menüyü veya açık bir açılır menüyü kapatmak için Esc tuşuna basın.

    \n' + + '\n' + + '

    Geçerli odak belirli bir kullanıcı arabirimi bölümünün "üst" kısmındaysa Esc tuşuna basıldığında\n' + + ' klavyeyle gezintiden de tamamen çıkılır.

    \n' + + '\n' + + '

    Menü öğesini veya araç çubuğu düğmesini yürütme

    \n' + + '\n' + + '

    İstediğiniz menü öğesi veya araç çubuğu düğmesi vurgulandığında Return, Enter\n' + + ' veya Ara çubuğu tuşuna basın.

    \n' + + '\n' + + '

    Sekme bulunmayan iletişim kutularında gezinme

    \n' + + '\n' + + '

    Sekme bulunmayan iletişim kutularında, iletişim kutusu açıldığında ilk etkileşimli bileşene odaklanılır.

    \n' + + '\n' + + '

    Etkileşimli iletişim kutusu bileşenleri arasında gezinmek için Sekme veya Shift+ Sekme tuşlarına basın.

    \n' + + '\n' + + '

    Sekmeli iletişim kutularında gezinme

    \n' + + '\n' + + '

    Sekmeli iletişim kutularında, iletişim kutusu açıldığında sekme menüsündeki ilk düğmeye odaklanılır.

    \n' + + '\n' + + '

    Bu iletişim kutusu sekmesinin etkileşimli bileşenleri arasında gezinmek için Sekme veya\n' + + ' Shift+Sekme tuşlarına basın.

    \n' + + '\n' + + '

    Mevcut sekmeler arasında geçiş yapmak için sekme menüsüne odaklanıp uygun Ok tuşuna basarak\n' + + ' başka bir iletişim kutusu sekmesine geçiş yapın.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/uk.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/uk.js new file mode 100644 index 0000000..028d4a4 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/uk.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.uk', +'

    Початок роботи з навігацією за допомогою клавіатури

    \n' + + '\n' + + '
    \n' + + '
    Фокус на рядок меню
    \n' + + '
    Windows або Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Фокус на панелі інструментів
    \n' + + '
    Windows або Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Фокус на розділі "Нижній колонтитул"
    \n' + + '
    Windows або Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Фокус на сповіщення
    \n' + + '
    Windows або Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Фокус на контекстній панелі інструментів
    \n' + + '
    Windows, Linux або macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Навігація почнеться з першого елемента інтерфейсу користувача, який буде виділено або підкреслено в разі, якщо перший елемент знаходиться в\n' + + ' шляху до елемента "Нижній колонтитул".

    \n' + + '\n' + + '

    Навігація між розділами інтерфейсу користувача

    \n' + + '\n' + + '

    Щоб перейти з одного розділу інтерфейсу користувача до наступного розділу, натисніть клавішу Tab.

    \n' + + '\n' + + '

    Щоб перейти з одного розділу інтерфейсу користувача до попереднього розділу, натисніть сполучення клавіш Shift+Tab.

    \n' + + '\n' + + '

    Порядок Вкладок цих розділів інтерфейсу користувача такий:

    \n' + + '\n' + + '
      \n' + + '
    1. Рядок меню
    2. \n' + + '
    3. Кожна група панелей інструментів
    4. \n' + + '
    5. Бічна панель
    6. \n' + + '
    7. Шлях до елементів у розділі "Нижній колонтитул"
    8. \n' + + '
    9. Кнопка перемикача "Кількість слів" у розділі "Нижній колонтитул"
    10. \n' + + '
    11. Посилання на брендинг у розділі "Нижній колонтитул"
    12. \n' + + '
    13. Маркер змінення розміру в розділі "Нижній колонтитул"
    14. \n' + + '
    \n' + + '\n' + + '

    Якщо розділ інтерфейсу користувача відсутній, він пропускається.

    \n' + + '\n' + + '

    Якщо фокус навігації клавіатури знаходиться на розділі "Нижній колонтитул", але користувач не бачить видиму бічну панель, натисніть Shift+Tab,\n' + + ' щоб перемістити фокус на першу групу панелі інструментів, а не на останню.

    \n' + + '\n' + + '

    Навігація в межах розділів інтерфейсу користувача

    \n' + + '\n' + + '

    Щоб перейти з одного елементу інтерфейсу користувача до наступного, натисніть відповідну клавішу зі стрілкою.

    \n' + + '\n' + + '

    Клавіші зі стрілками Ліворуч і Праворуч

    \n' + + '\n' + + '
      \n' + + '
    • переміщують між меню в рядку меню.
    • \n' + + '
    • відкривають вкладене меню в меню.
    • \n' + + '
    • переміщують користувача між кнопками в групі панелі інструментів.
    • \n' + + '
    • переміщують між елементами в шляху до елементів у розділі "Нижній колонтитул".
    • \n' + + '
    \n' + + '\n' + + '

    Клавіші зі стрілками Вниз і Вгору

    \n' + + '\n' + + '
      \n' + + '
    • переміщують між елементами меню в меню.
    • \n' + + '
    • переміщують між елементами в спливаючому меню панелі інструментів.
    • \n' + + '
    \n' + + '\n' + + '

    Клавіші зі стрілками переміщують фокус циклічно в межах розділу інтерфейсу користувача, на якому знаходиться фокус.

    \n' + + '\n' + + '

    Щоб закрити відкрите меню, відкрите вкладене меню або відкрите спливаюче меню, натисніть клавішу Esc.

    \n' + + '\n' + + '

    Якщо поточний фокус знаходиться на верхньому рівні певного розділу інтерфейсу користувача, натискання клавіші Esc також виконує вихід\n' + + ' з навігації за допомогою клавіатури повністю.

    \n' + + '\n' + + '

    Виконання елементу меню або кнопки панелі інструментів

    \n' + + '\n' + + '

    Коли потрібний елемент меню або кнопку панелі інструментів виділено, натисніть клавіші Return, Enter,\n' + + ' або Пробіл, щоб виконати цей елемент.

    \n' + + '\n' + + '

    Навігація по діалоговим вікнам без вкладок

    \n' + + '\n' + + '

    У діалогових вікнах без вкладок перший інтерактивний компонент приймає фокус, коли відкривається діалогове вікно.

    \n' + + '\n' + + '

    Переходьте між інтерактивними компонентами діалогового вікна, натискаючи клавіші Tab або Shift+Tab.

    \n' + + '\n' + + '

    Навігація по діалоговим вікнам з вкладками

    \n' + + '\n' + + '

    У діалогових вікнах із вкладками перша кнопка в меню вкладки приймає фокус, коли відкривається діалогове вікно.

    \n' + + '\n' + + '

    Переходьте між інтерактивними компонентами цієї вкладки діалогового вікна, натискаючи клавіші Tab або\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Щоб перейти на іншу вкладку діалогового вікна, перемістіть фокус на меню вкладки, а потім натисніть відповідну клавішу зі стрілкою,\n' + + ' щоб циклічно переходити по доступним вкладкам.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/vi.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/vi.js new file mode 100644 index 0000000..d8eda11 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/vi.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.vi', +'

    Bắt đầu điều hướng bàn phím

    \n' + + '\n' + + '
    \n' + + '
    Tập trung vào thanh menu
    \n' + + '
    Windows hoặc Linux: Alt+F9
    \n' + + '
    macOS: ⌥F9
    \n' + + '
    Tập trung vào thanh công cụ
    \n' + + '
    Windows hoặc Linux: Alt+F10
    \n' + + '
    macOS: ⌥F10
    \n' + + '
    Tập trung vào chân trang
    \n' + + '
    Windows hoặc Linux: Alt+F11
    \n' + + '
    macOS: ⌥F11
    \n' + + '
    Tập trung vào thông báo
    \n' + + '
    Windows hoặc Linux: Alt+F12
    \n' + + '
    macOS: ⌥F12
    \n' + + '
    Tập trung vào thanh công cụ ngữ cảnh
    \n' + + '
    Windows, Linux hoặc macOS: Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    Điều hướng sẽ bắt đầu từ mục UI đầu tiên. Mục này sẽ được tô sáng hoặc có gạch dưới (nếu là mục đầu tiên trong\n' + + ' đường dẫn phần tử Chân trang).

    \n' + + '\n' + + '

    Di chuyển qua lại giữa các phần UI

    \n' + + '\n' + + '

    Để di chuyển từ một phần UI sang phần tiếp theo, ấn Tab.

    \n' + + '\n' + + '

    Để di chuyển từ một phần UI về phần trước đó, ấn Shift+Tab.

    \n' + + '\n' + + '

    Thứ tự Tab của các phần UI này như sau:

    \n' + + '\n' + + '
      \n' + + '
    1. Thanh menu
    2. \n' + + '
    3. Từng nhóm thanh công cụ
    4. \n' + + '
    5. Thanh bên
    6. \n' + + '
    7. Đường dẫn phần tử trong chân trang
    8. \n' + + '
    9. Nút chuyển đổi đếm chữ ở chân trang
    10. \n' + + '
    11. Liên kết thương hiệu ở chân trang
    12. \n' + + '
    13. Núm điều tác chỉnh kích cỡ trình soạn thảo ở chân trang
    14. \n' + + '
    \n' + + '\n' + + '

    Nếu người dùng không thấy một phần UI, thì có nghĩa phần đó bị bỏ qua.

    \n' + + '\n' + + '

    Nếu ở chân trang có tính năng tập trung điều hướng bàn phím, mà không có thanh bên nào hiện hữu, thao tác ấn Shift+Tab\n' + + ' sẽ chuyển hướng tập trung vào nhóm thanh công cụ đầu tiên, không phải cuối cùng.

    \n' + + '\n' + + '

    Di chuyển qua lại trong các phần UI

    \n' + + '\n' + + '

    Để di chuyển từ một phần tử UI sang phần tiếp theo, ấn phím Mũi tên tương ứng cho phù hợp.

    \n' + + '\n' + + '

    Các phím mũi tên TráiPhải

    \n' + + '\n' + + '
      \n' + + '
    • di chuyển giữa các menu trong thanh menu.
    • \n' + + '
    • mở menu phụ trong một menu.
    • \n' + + '
    • di chuyển giữa các nút trong nhóm thanh công cụ.
    • \n' + + '
    • di chuyển giữa các mục trong đường dẫn phần tử của chân trang.
    • \n' + + '
    \n' + + '\n' + + '

    Các phím mũi tên Hướng xuốngHướng lên

    \n' + + '\n' + + '
      \n' + + '
    • di chuyển giữa các mục menu trong menu.
    • \n' + + '
    • di chuyển giữa các mục trong menu thanh công cụ dạng bật lên.
    • \n' + + '
    \n' + + '\n' + + '

    Các phím mũi tên xoay vòng trong một phần UI tập trung.

    \n' + + '\n' + + '

    Để đóng một menu mở, một menu phụ đang mở, hoặc một menu dạng bật lên đang mở, hãy ấn phím Esc.

    \n' + + '\n' + + '

    Nếu trọng tâm hiện tại là ở phần “đầu” của một phần UI cụ thể, thao tác ấn phím Esc cũng sẽ thoát\n' + + ' toàn bộ phần điều hướng bàn phím.

    \n' + + '\n' + + '

    Thực hiện chức năng của một mục menu hoặc nút thanh công cụ

    \n' + + '\n' + + '

    Khi mục menu hoặc nút thanh công cụ muốn dùng được tô sáng, hãy ấn Return, Enter,\n' + + ' hoặc Phím cách để thực hiện chức năng mục đó.

    \n' + + '\n' + + '

    Điều hướng giữa các hộp thoại không có nhiều tab

    \n' + + '\n' + + '

    Trong các hộp thoại không có nhiều tab, khi hộp thoại mở ra, trọng tâm sẽ hướng vào thành phần tương tác đầu tiên.

    \n' + + '\n' + + '

    Di chuyển giữa các thành phần hộp thoại tương tác bằng cách ấn Tab hoặc Shift+Tab.

    \n' + + '\n' + + '

    Điều hướng giữa các hộp thoại có nhiều tab

    \n' + + '\n' + + '

    Trong các hộp thoại có nhiều tab, khi hộp thoại mở ra, trọng tâm sẽ hướng vào nút đầu tiên trong menu tab.

    \n' + + '\n' + + '

    Di chuyển giữa các thành phần tương tác của tab hộp thoại này bằng cách ấn Tab hoặc\n' + + ' Shift+Tab.

    \n' + + '\n' + + '

    Chuyển sang một tab hộp thoại khác bằng cách chuyển trọng tâm vào menu tab, rồi ấn phím Mũi tên phù hợp\n' + + ' để xoay vòng các tab hiện có.

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/zh_CN.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/zh_CN.js new file mode 100644 index 0000000..f7e73d1 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/zh_CN.js @@ -0,0 +1,87 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.zh_CN', +'

    开始键盘导航

    \n' + + '\n' + + '
    \n' + + '
    使菜单栏处于焦点
    \n' + + '
    Windows 或 Linux:Alt+F9
    \n' + + '
    macOS:⌥F9
    \n' + + '
    使工具栏处于焦点
    \n' + + '
    Windows 或 Linux:Alt+F10
    \n' + + '
    macOS:⌥F10
    \n' + + '
    使页脚处于焦点
    \n' + + '
    Windows 或 Linux:Alt+F11
    \n' + + '
    macOS:⌥F11
    \n' + + '
    使通知处于焦点
    \n' + + '
    Windows 或 Linux:Alt+F12
    \n' + + '
    macOS:⌥F12
    \n' + + '
    使上下文工具栏处于焦点
    \n' + + '
    Windows、Linux 或 macOS:Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    导航将在第一个 UI 项上开始,其中突出显示该项,或者对于页脚元素路径中的第一项,将为其添加下划线。

    \n' + + '\n' + + '

    在 UI 部分之间导航

    \n' + + '\n' + + '

    要从一个 UI 部分移至下一个,请按 Tab

    \n' + + '\n' + + '

    要从一个 UI 部分移至上一个,请按 Shift+Tab

    \n' + + '\n' + + '

    这些 UI 部分的 Tab 顺序为:

    \n' + + '\n' + + '
      \n' + + '
    1. 菜单栏
    2. \n' + + '
    3. 每个工具栏组
    4. \n' + + '
    5. 边栏
    6. \n' + + '
    7. 页脚中的元素路径
    8. \n' + + '
    9. 页脚中的字数切换按钮
    10. \n' + + '
    11. 页脚中的品牌链接
    12. \n' + + '
    13. 页脚中的编辑器调整大小图柄
    14. \n' + + '
    \n' + + '\n' + + '

    如果不存在某个 UI 部分,则跳过它。

    \n' + + '\n' + + '

    如果键盘导航焦点在页脚,并且没有可见的边栏,则按 Shift+Tab 将焦点移至第一个工具栏组而非最后一个。

    \n' + + '\n' + + '

    在 UI 部分内导航

    \n' + + '\n' + + '

    要从一个 UI 元素移至下一个,请按相应的箭头键。

    \n' + + '\n' + + '

    箭头键

    \n' + + '\n' + + '
      \n' + + '
    • 在菜单栏中的菜单之间移动。
    • \n' + + '
    • 打开菜单中的子菜单。
    • \n' + + '
    • 在工具栏组中的按钮之间移动。
    • \n' + + '
    • 在页脚的元素路径中的各项之间移动。
    • \n' + + '
    \n' + + '\n' + + '

    箭头键

    \n' + + '\n' + + '
      \n' + + '
    • 在菜单中的菜单项之间移动。
    • \n' + + '
    • 在工具栏弹出菜单中的各项之间移动。
    • \n' + + '
    \n' + + '\n' + + '

    箭头键在具有焦点的 UI 部分内循环。

    \n' + + '\n' + + '

    要关闭打开的菜单、打开的子菜单或打开的弹出菜单,请按 Esc 键。

    \n' + + '\n' + + '

    如果当前的焦点在特定 UI 部分的“顶部”,则按 Esc 键还将完全退出键盘导航。

    \n' + + '\n' + + '

    执行菜单项或工具栏按钮

    \n' + + '\n' + + '

    当突出显示所需的菜单项或工具栏按钮时,按 ReturnEnter空格以执行该项。

    \n' + + '\n' + + '

    在非标签页式对话框中导航

    \n' + + '\n' + + '

    在非标签页式对话框中,当对话框打开时,第一个交互组件获得焦点。

    \n' + + '\n' + + '

    通过按 TabShift+Tab,在交互对话框组件之间导航。

    \n' + + '\n' + + '

    在标签页式对话框中导航

    \n' + + '\n' + + '

    在标签页式对话框中,当对话框打开时,标签页菜单中的第一个按钮获得焦点。

    \n' + + '\n' + + '

    通过按 TabShift+Tab,在此对话框的交互组件之间导航。

    \n' + + '\n' + + '

    通过将焦点移至另一对话框标签页的菜单,然后按相应的箭头键以在可用的标签页间循环,从而切换到该对话框标签页。

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/zh_TW.js b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/zh_TW.js new file mode 100644 index 0000000..5912770 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/js/i18n/keynav/zh_TW.js @@ -0,0 +1,93 @@ +tinymce.Resource.add('tinymce.html-i18n.help-keynav.zh_TW', +'

    開始鍵盤瀏覽

    \n' + + '\n' + + '
    \n' + + '
    跳至功能表列
    \n' + + '
    Windows 或 Linux:Alt+F9
    \n' + + '
    macOS:⌥F9
    \n' + + '
    跳至工具列
    \n' + + '
    Windows 或 Linux:Alt+F10
    \n' + + '
    macOS:⌥F10
    \n' + + '
    跳至頁尾
    \n' + + '
    Windows 或 Linux:Alt+F11
    \n' + + '
    macOS:⌥F11
    \n' + + '
    跳至通知
    \n' + + '
    Windows 或 Linux:Alt+F12
    \n' + + '
    macOS:⌥F12
    \n' + + '
    跳至關聯式工具列
    \n' + + '
    Windows、Linux 或 macOS:Ctrl+F9
    \n' + + '
    \n' + + '\n' + + '

    瀏覽會從第一個 UI 項目開始,該項目會反白顯示,但如果是「頁尾」元素路徑的第一項,\n' + + ' 則加底線。

    \n' + + '\n' + + '

    在 UI 區段之間瀏覽

    \n' + + '\n' + + '

    從 UI 區段移至下一個,請按 Tab

    \n' + + '\n' + + '

    從 UI 區段移回上一個,請按 Shift+Tab

    \n' + + '\n' + + '

    這些 UI 區段的 Tab 順序如下:

    \n' + + '\n' + + '
      \n' + + '
    1. 功能表列
    2. \n' + + '
    3. 各個工具列群組
    4. \n' + + '
    5. 側邊欄
    6. \n' + + '
    7. 頁尾中的元素路徑
    8. \n' + + '
    9. 頁尾中字數切換按鈕
    10. \n' + + '
    11. 頁尾中的品牌連結
    12. \n' + + '
    13. 頁尾中編輯器調整大小控點
    14. \n' + + '
    \n' + + '\n' + + '

    如果 UI 區段未顯示,表示已略過該區段。

    \n' + + '\n' + + '

    如果鍵盤瀏覽跳至頁尾,但沒有顯示側邊欄,則按下 Shift+Tab\n' + + ' 會跳至第一個工具列群組,而不是最後一個。

    \n' + + '\n' + + '

    在 UI 區段之內瀏覽

    \n' + + '\n' + + '

    在兩個 UI 元素之間移動,請按適當的方向鍵。

    \n' + + '\n' + + '

    向左向右方向鍵

    \n' + + '\n' + + '
      \n' + + '
    • 在功能表列中的功能表之間移動。
    • \n' + + '
    • 開啟功能表中的子功能表。
    • \n' + + '
    • 在工具列群組中的按鈕之間移動。
    • \n' + + '
    • 在頁尾的元素路徑中項目之間移動。
    • \n' + + '
    \n' + + '\n' + + '

    向下向上方向鍵

    \n' + + '\n' + + '
      \n' + + '
    • 在功能表中的功能表項目之間移動。
    • \n' + + '
    • 在工具列快顯功能表中的項目之間移動。
    • \n' + + '
    \n' + + '\n' + + '

    方向鍵會在所跳至 UI 區段之內循環。

    \n' + + '\n' + + '

    若要關閉已開啟的功能表、已開啟的子功能表,或已開啟的快顯功能表,請按 Esc 鍵。

    \n' + + '\n' + + '

    如果目前已跳至特定 UI 區段的「頂端」,則按 Esc 鍵也會結束\n' + + ' 整個鍵盤瀏覽。

    \n' + + '\n' + + '

    執行功能表列項目或工具列按鈕

    \n' + + '\n' + + '

    當想要的功能表項目或工具列按鈕已反白顯示時,按 ReturnEnter、\n' + + ' 或空白鍵即可執行該項目。

    \n' + + '\n' + + '

    瀏覽非索引標籤式對話方塊

    \n' + + '\n' + + '

    在非索引標籤式對話方塊中,開啟對話方塊時會跳至第一個互動元件。

    \n' + + '\n' + + '

    TabShift+Tab 即可在互動式對話方塊元件之間瀏覽。

    \n' + + '\n' + + '

    瀏覽索引標籤式對話方塊

    \n' + + '\n' + + '

    在索引標籤式對話方塊中,開啟對話方塊時會跳至索引標籤式功能表中的第一個按鈕。

    \n' + + '\n' + + '

    若要在此對話方塊的互動式元件之間瀏覽,請按 Tab 或\n' + + ' Shift+Tab

    \n' + + '\n' + + '

    先跳至索引標籤式功能表,然後按適當的方向鍵,即可切換至另一個對話方塊索引標籤,\n' + + ' 以循環瀏覽可用的索引標籤。

    \n'); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/help/plugin.min.js b/src/main/webapp/content/tinymce/plugins/help/plugin.min.js new file mode 100644 index 0000000..acddac2 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/help/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");let t=0;const n=e=>{const n=(new Date).getTime(),a=Math.floor(window.crypto.getRandomValues(new Uint32Array(1))[0]/4294967295*1e9);return t++,e+"_"+a+t+String(n)},a=e=>t=>t.options.get(e),r=a("help_tabs"),o=a("forced_plugins"),i=e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=a=e,(r=String).prototype.isPrototypeOf(n)||(null===(o=a.constructor)||void 0===o?void 0:o.name)===r.name)?"string":t;var n,a,r,o})(e);const s=e=>undefined===e;const c=e=>"function"==typeof e,l=()=>false;class m{constructor(e,t){this.tag=e,this.value=t}static some(e){return new m(!0,e)}static none(){return m.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?m.some(e(this.value)):m.none()}bind(e){return this.tag?e(this.value):m.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:m.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return null==e?m.none():m.some(e)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}m.singletonNone=new m(!1);const u=Array.prototype.slice,p=Array.prototype.indexOf,y=(e,t)=>{const n=e.length,a=new Array(n);for(let r=0;r{const n=[];for(let a=0,r=e.length;a{const n=u.call(e,0);return n.sort(t),n},g=Object.keys,k=Object.hasOwnProperty,v=(e,t)=>k.call(e,t);var b=tinymce.util.Tools.resolve("tinymce.Resource"),f=tinymce.util.Tools.resolve("tinymce.util.I18n");const A=(e,t)=>b.load(`tinymce.html-i18n.help-keynav.${t}`,`${e}/js/i18n/keynav/${t}.js`),w=e=>A(e,f.getCode()).catch((()=>A(e,"en")));var C=tinymce.util.Tools.resolve("tinymce.Env");const S=e=>{const t=C.os.isMacOS()||C.os.isiOS(),n=t?{alt:"⌥",ctrl:"⌃",shift:"⇧",meta:"⌘",access:"⌃⌥"}:{meta:"Ctrl ",access:"Shift + Alt "},a=e.split("+"),r=y(a,(e=>{const t=e.toLowerCase().trim();return v(n,t)?n[t]:e}));return t?r.join("").replace(/\s/,""):r.join("+")},M=[{shortcuts:["Meta + B"],action:"Bold"},{shortcuts:["Meta + I"],action:"Italic"},{shortcuts:["Meta + U"],action:"Underline"},{shortcuts:["Meta + A"],action:"Select all"},{shortcuts:["Meta + Y","Meta + Shift + Z"],action:"Redo"},{shortcuts:["Meta + Z"],action:"Undo"},{shortcuts:["Access + 1"],action:"Heading 1"},{shortcuts:["Access + 2"],action:"Heading 2"},{shortcuts:["Access + 3"],action:"Heading 3"},{shortcuts:["Access + 4"],action:"Heading 4"},{shortcuts:["Access + 5"],action:"Heading 5"},{shortcuts:["Access + 6"],action:"Heading 6"},{shortcuts:["Access + 7"],action:"Paragraph"},{shortcuts:["Access + 8"],action:"Div"},{shortcuts:["Access + 9"],action:"Address"},{shortcuts:["Alt + 0"],action:"Open help dialog"},{shortcuts:["Alt + F9"],action:"Focus to menubar"},{shortcuts:["Alt + F10"],action:"Focus to toolbar"},{shortcuts:["Alt + F11"],action:"Focus to element path"},{shortcuts:["Alt + F12"],action:"Focus to notification"},{shortcuts:["Ctrl + F9"],action:"Focus to contextual toolbar"},{shortcuts:["Shift + Enter"],action:"Open popup menu for split buttons"},{shortcuts:["Meta + K"],action:"Insert link (if link plugin activated)"},{shortcuts:["Meta + S"],action:"Save (if save plugin activated)"},{shortcuts:["Meta + F"],action:"Find (if searchreplace plugin activated)"},{shortcuts:["Meta + Shift + F"],action:"Switch to or from fullscreen mode"}],_=()=>({name:"shortcuts",title:"Handy Shortcuts",items:[{type:"table",header:["Action","Shortcut"],cells:y(M,(e=>{const t=y(e.shortcuts,S).join(" or ");return[e.action,t]}))}]}),x=y([{key:"accordion",name:"Accordion"},{key:"anchor",name:"Anchor"},{key:"autolink",name:"Autolink"},{key:"autoresize",name:"Autoresize"},{key:"autosave",name:"Autosave"},{key:"charmap",name:"Character Map"},{key:"code",name:"Code"},{key:"codesample",name:"Code Sample"},{key:"colorpicker",name:"Color Picker"},{key:"directionality",name:"Directionality"},{key:"emoticons",name:"Emoticons"},{key:"fullscreen",name:"Full Screen"},{key:"help",name:"Help"},{key:"image",name:"Image"},{key:"importcss",name:"Import CSS"},{key:"insertdatetime",name:"Insert Date/Time"},{key:"link",name:"Link"},{key:"lists",name:"Lists"},{key:"advlist",name:"List Styles"},{key:"media",name:"Media"},{key:"nonbreaking",name:"Nonbreaking"},{key:"pagebreak",name:"Page Break"},{key:"preview",name:"Preview"},{key:"quickbars",name:"Quick Toolbars"},{key:"save",name:"Save"},{key:"searchreplace",name:"Search and Replace"},{key:"table",name:"Table"},{key:"textcolor",name:"Text Color"},{key:"visualblocks",name:"Visual Blocks"},{key:"visualchars",name:"Visual Characters"},{key:"wordcount",name:"Word Count"},{key:"a11ychecker",name:"Accessibility Checker",type:"premium"},{key:"typography",name:"Advanced Typography",type:"premium",slug:"advanced-typography"},{key:"ai",name:"AI Assistant",type:"premium"},{key:"casechange",name:"Case Change",type:"premium"},{key:"checklist",name:"Checklist",type:"premium"},{key:"advcode",name:"Enhanced Code Editor",type:"premium"},{key:"mediaembed",name:"Enhanced Media Embed",type:"premium",slug:"introduction-to-mediaembed"},{key:"advtable",name:"Enhanced Tables",type:"premium"},{key:"exportpdf",name:"Export to PDF",type:"premium"},{key:"exportword",name:"Export to Word",type:"premium"},{key:"footnotes",name:"Footnotes",type:"premium"},{key:"formatpainter",name:"Format Painter",type:"premium"},{key:"editimage",name:"Image Editing",type:"premium"},{key:"uploadcare",name:"Image Optimizer Powered by Uploadcare",type:"premium"},{key:"importword",name:"Import from Word",type:"premium"},{key:"inlinecss",name:"Inline CSS",type:"premium",slug:"inline-css"},{key:"linkchecker",name:"Link Checker",type:"premium"},{key:"math",name:"Math",type:"premium"},{key:"markdown",name:"Markdown",type:"premium"},{key:"mentions",name:"Mentions",type:"premium"},{key:"mergetags",name:"Merge Tags",type:"premium"},{key:"pageembed",name:"Page Embed",type:"premium"},{key:"permanentpen",name:"Permanent Pen",type:"premium"},{key:"powerpaste",name:"PowerPaste",type:"premium",slug:"introduction-to-powerpaste"},{key:"revisionhistory",name:"Revision History",type:"premium"},{key:"tinymcespellchecker",name:"Spell Checker",type:"premium",slug:"introduction-to-tiny-spellchecker"},{key:"autocorrect",name:"Spelling Autocorrect",type:"premium"},{key:"tableofcontents",name:"Table of Contents",type:"premium"},{key:"advtemplate",name:"Templates",type:"premium",slug:"advanced-templates"},{key:"tinycomments",name:"Tiny Comments",type:"premium",slug:"introduction-to-tiny-comments"},{key:"tinydrive",name:"Tiny Drive",type:"premium",slug:"tinydrive-introduction"}],(e=>({...e,type:e.type||"opensource",slug:e.slug||e.key}))),T=e=>{const t=e=>`${e.name}`,n=(e,n)=>{return(a=x,r=e=>e.key===n,((e,t,n)=>{for(let a=0,r=e.length;a((e,n)=>{const a=e.plugins[n].getMetadata;if(c(a)){const e=a();return{name:e.name,html:t(e)}}return{name:n,html:n}})(e,n)),(e=>{const n="premium"===e.type?`${e.name}*`:e.name;return{name:n,html:t({name:n,url:`https://www.tiny.cloud/docs/tinymce/7/${e.slug}/`})}}));var a,r},a=e=>{const t=(e=>{const t=g(e.plugins),n=o(e),a=s(n)?["onboarding"]:n.concat(["onboarding"]);return h(t,(e=>!(((e,t)=>p.call(e,t))(a,e)>-1)))})(e),a=d(y(t,(t=>n(e,t))),((e,t)=>e.name.localeCompare(t.name))),r=y(a,(e=>"
  • "+e.html+"
  • ")),i=r.length,c=r.join("");return"

    "+f.translate(["Plugins installed ({0}):",i])+"

      "+c+"
    "},r={type:"htmlpanel",presets:"document",html:[(e=>null==e?"":"
    "+a(e)+"
    ")(e),(()=>{const e=h(x,(({type:e})=>"premium"===e)),t=d(y(e,(e=>e.name)),((e,t)=>e.localeCompare(t))),n=y(t,(e=>`
  • ${e}
  • `)).join("");return"

    "+f.translate("Premium plugins:")+"

    "})()].join("")};return{name:"plugins",title:"Plugins",items:[r]}};var O=tinymce.util.Tools.resolve("tinymce.EditorManager");const P=(e,t,a)=>()=>{(async(e,t,a)=>{const o=_(),s=await(async e=>({name:"keyboardnav",title:"Keyboard Navigation",items:[{type:"htmlpanel",presets:"document",html:await w(e)}]}))(a),c=T(e),l=(()=>{var e,t;const n='TinyMCE '+(e=O.majorVersion,t=O.minorVersion,(0===e.indexOf("@")?"X.X.X":e+"."+t)+"");return{name:"versions",title:"Version",items:[{type:"htmlpanel",html:"

    "+f.translate(["You are using {0}",n])+"

    ",presets:"document"}]}})(),u={[o.name]:o,[s.name]:s,[c.name]:c,[l.name]:l,...t.get()};return m.from(r(e)).fold((()=>(e=>{const t=g(e),n=t.indexOf("versions");return-1!==n&&(t.splice(n,1),t.push("versions")),{tabs:e,names:t}})(u)),(e=>((e,t)=>{const a={},r=y(e,(e=>{var r;if(i(e))return v(t,e)&&(a[e]=t[e]),e;{const t=null!==(r=e.name)&&void 0!==r?r:n("tab-name");return a[t]=e,t}}));return{tabs:a,names:r}})(e,u)))})(e,t,a).then((({tabs:t,names:n})=>{const a={type:"tabpanel",tabs:(e=>{const t=[],n=e=>{t.push(e)};for(let t=0;t{return v(n=t,a=e)?m.from(n[a]):m.none();var n,a})))};e.windowManager.open({title:"Help",size:"medium",body:a,buttons:[{type:"cancel",name:"close",text:"Close",primary:!0}],initialData:{}})}))};e.add("help",((e,t)=>{const a=(()=>{let e={};return{get:()=>e,set:t=>{e=t}}})(),r=(e=>({addTab:t=>{var a;const r=null!==(a=t.name)&&void 0!==a?a:n("tab-name"),o=e.get();o[r]=t,e.set(o)}}))(a);(e=>{(0,e.options.register)("help_tabs",{processor:"array"})})(e);const o=P(e,a,t);return((e,t)=>{e.ui.registry.addButton("help",{icon:"help",tooltip:"Help",onAction:t,context:"any"}),e.ui.registry.addMenuItem("help",{text:"Help",icon:"help",shortcut:"Alt+0",onAction:t,context:"any"})})(e,o),((e,t)=>{e.addCommand("mceHelp",t)})(e,o),e.shortcuts.add("Alt+0","Open help dialog","mceHelp"),((e,t)=>{e.on("init",(()=>{w(t)}))})(e,t),r}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/image/plugin.min.js b/src/main/webapp/content/tinymce/plugins/image/plugin.min.js new file mode 100644 index 0000000..9480158 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/image/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=Object.getPrototypeOf,a=(e,t,a)=>{var i;return!!a(e,t.prototype)||(null===(i=e.constructor)||void 0===i?void 0:i.name)===t.name},i=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&a(e,String,((e,t)=>t.isPrototypeOf(e)))?"string":t})(t)===e,s=e=>t=>typeof t===e,r=i("string"),o=i("object"),n=e=>((e,i)=>o(e)&&a(e,i,((e,a)=>t(e)===a)))(e,Object),l=i("array"),c=e=>null===e;const m=s("boolean"),d=e=>!(e=>null==e)(e),g=s("function"),u=s("number"),p=()=>{};class h{constructor(e,t){this.tag=e,this.value=t}static some(e){return new h(!0,e)}static none(){return h.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?h.some(e(this.value)):h.none()}bind(e){return this.tag?e(this.value):h.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:h.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return d(e)?h.some(e):h.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}h.singletonNone=new h(!1);const b=Object.keys,v=Object.hasOwnProperty,y=(e,t)=>v.call(e,t),f=Array.prototype.push,w=e=>{const t=[];for(let a=0,i=e.length;a{((e,t,a)=>{if(!(r(a)||m(a)||u(a)))throw console.error("Invalid call to Attribute.set. Key ",t,":: Value ",a,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,a+"")})(e.dom,t,a)},D=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},_=D;var C=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),I=tinymce.util.Tools.resolve("tinymce.util.URI");const U=e=>e.length>0,S=e=>t=>t.options.get(e),x=S("image_dimensions"),N=S("image_advtab"),T=S("image_uploadtab"),E=S("image_prepend_url"),L=S("image_class_list"),O=S("image_description"),j=S("image_title"),M=S("image_caption"),R=S("image_list"),k=S("a11y_advanced_options"),z=S("automatic_uploads"),B=(e,t)=>Math.max(parseInt(e,10),parseInt(t,10)),P=e=>(e&&(e=e.replace(/px$/,"")),e),F=e=>(e.length>0&&/^[0-9]+$/.test(e)&&(e+="px"),e),H=e=>"IMG"===e.nodeName&&(e.hasAttribute("data-mce-object")||e.hasAttribute("data-mce-placeholder")),G=(e,t)=>{const a=e.options.get;return I.isDomSafe(t,"img",{allow_html_data_urls:a("allow_html_data_urls"),allow_script_urls:a("allow_script_urls"),allow_svg_data_urls:a("allow_svg_data_urls")})},W=C.DOM,$=e=>e.style.marginLeft&&e.style.marginRight&&e.style.marginLeft===e.style.marginRight?P(e.style.marginLeft):"",V=e=>e.style.marginTop&&e.style.marginBottom&&e.style.marginTop===e.style.marginBottom?P(e.style.marginTop):"",K=e=>e.style.borderWidth?P(e.style.borderWidth):"",Z=(e,t)=>{var a;return e.hasAttribute(t)&&null!==(a=e.getAttribute(t))&&void 0!==a?a:""},q=e=>null!==e.parentNode&&"FIGURE"===e.parentNode.nodeName,J=(e,t,a)=>{""===a||null===a?e.removeAttribute(t):e.setAttribute(t,a)},Q=(e,t)=>{const a=e.getAttribute("style"),i=t(null!==a?a:"");i.length>0?(e.setAttribute("style",i),e.setAttribute("data-mce-style",i)):e.removeAttribute("style")},X=(e,t)=>(e,a,i)=>{const s=e.style;s[a]?(s[a]=F(i),Q(e,t)):J(e,a,i)},Y=(e,t)=>e.style[t]?P(e.style[t]):Z(e,t),ee=(e,t)=>{const a=F(t);e.style.marginLeft=a,e.style.marginRight=a},te=(e,t)=>{const a=F(t);e.style.marginTop=a,e.style.marginBottom=a},ae=(e,t)=>{const a=F(t);e.style.borderWidth=a},ie=(e,t)=>{e.style.borderStyle=t},se=e=>{var t;return null!==(t=e.style.borderStyle)&&void 0!==t?t:""},re=e=>d(e)&&"FIGURE"===e.nodeName,oe=e=>0===W.getAttrib(e,"alt").length&&"presentation"===W.getAttrib(e,"role"),ne=e=>oe(e)?"":Z(e,"alt"),le=(e,t)=>{var a;const i=document.createElement("img");return J(i,"style",t.style),($(i)||""!==t.hspace)&&ee(i,t.hspace),(V(i)||""!==t.vspace)&&te(i,t.vspace),(K(i)||""!==t.border)&&ae(i,t.border),(se(i)||""!==t.borderStyle)&&ie(i,t.borderStyle),e(null!==(a=i.getAttribute("style"))&&void 0!==a?a:"")},ce=(e,t)=>({src:Z(t,"src"),alt:ne(t),title:Z(t,"title"),width:Y(t,"width"),height:Y(t,"height"),class:Z(t,"class"),style:e(Z(t,"style")),caption:q(t),hspace:$(t),vspace:V(t),border:K(t),borderStyle:se(t),isDecorative:oe(t)}),me=(e,t,a,i,s)=>{a[i]!==t[i]&&s(e,i,String(a[i]))},de=(e,t,a)=>{if(a){W.setAttrib(e,"role","presentation");const t=_(e);A(t,"alt","")}else{if(c(t)){_(e).dom.removeAttribute("alt")}else{const a=_(e);A(a,"alt",t)}"presentation"===W.getAttrib(e,"role")&&W.setAttrib(e,"role","")}},ge=(e,t)=>(a,i,s)=>{e(a,s),Q(a,t)},ue=(e,t,a)=>{const i=ce(e,a);me(a,i,t,"caption",((e,t,a)=>(e=>{q(e)?(e=>{const t=e.parentNode;d(t)&&(W.insertAfter(e,t),W.remove(t))})(e):(e=>{const t=W.create("figure",{class:"image"});W.insertAfter(t,e),t.appendChild(e),t.appendChild(W.create("figcaption",{contentEditable:"true"},"Caption")),t.contentEditable="false"})(e)})(e))),me(a,i,t,"src",J),me(a,i,t,"title",J),me(a,i,t,"width",X(0,e)),me(a,i,t,"height",X(0,e)),me(a,i,t,"class",J),me(a,i,t,"style",ge(((e,t)=>J(e,"style",t)),e)),me(a,i,t,"hspace",ge(ee,e)),me(a,i,t,"vspace",ge(te,e)),me(a,i,t,"border",ge(ae,e)),me(a,i,t,"borderStyle",ge(ie,e)),((e,t,a)=>{a.alt===t.alt&&a.isDecorative===t.isDecorative||de(e,a.alt,a.isDecorative)})(a,i,t)},pe=(e,t)=>{const a=(e=>{if(e.margin){const t=String(e.margin).split(" ");switch(t.length){case 1:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[0],e["margin-bottom"]=e["margin-bottom"]||t[0],e["margin-left"]=e["margin-left"]||t[0];break;case 2:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[0],e["margin-left"]=e["margin-left"]||t[1];break;case 3:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[2],e["margin-left"]=e["margin-left"]||t[1];break;case 4:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[2],e["margin-left"]=e["margin-left"]||t[3]}delete e.margin}return e})(e.dom.styles.parse(t)),i=e.dom.styles.parse(e.dom.styles.serialize(a));return e.dom.styles.serialize(i)},he=e=>{const t=e.selection.getNode(),a=e.dom.getParent(t,"figure.image");return a?e.dom.select("img",a)[0]:t&&("IMG"!==t.nodeName||H(t))?null:t},be=(e,t)=>{var a;const i=e.dom,s=(t=>{const a={};var i;return((e,t,a,i)=>{((e,t)=>{const a=b(e);for(let i=0,s=a.length;i{(t(e,s)?a:i)(e,s)}))})(t,((t,a)=>!e.schema.isValidChild(a,"figure")),(i=a,(e,t)=>{i[t]=e}),p),a})(e.schema.getTextBlockElements()),r=i.getParent(t.parentNode,(e=>{return t=s,a=e.nodeName,y(t,a)&&void 0!==t[a]&&null!==t[a];var t,a}),e.getBody());return r&&null!==(a=i.split(r,t))&&void 0!==a?a:t},ve=(e,t)=>{const a=((t,a)=>{const i=document.createElement("img");if(ue((t=>pe(e,t)),{...a,caption:!1},i),de(i,a.alt,a.isDecorative),a.caption){const e=W.create("figure",{class:"image"});return e.appendChild(i),e.appendChild(W.create("figcaption",{contentEditable:"true"},"Caption")),e.contentEditable="false",e}return i})(0,t);e.dom.setAttrib(a,"data-mce-id","__mcenew"),e.focus(),e.selection.setContent(a.outerHTML);const i=e.dom.select('*[data-mce-id="__mcenew"]')[0];if(e.dom.setAttrib(i,"data-mce-id",null),re(i)){const t=be(e,i);e.selection.select(t)}else e.selection.select(i)},ye=(e,t)=>{const a=he(e);if(a){const i={...ce((t=>pe(e,t)),a),...t},s=((e,t)=>{const a=t.src;return{...t,src:G(e,a)?a:""}})(e,i);i.src?((e,t)=>{const a=he(e);if(a)if(ue((t=>pe(e,t)),t,a),((e,t)=>{e.dom.setAttrib(t,"src",t.getAttribute("src"))})(e,a),re(a.parentNode)){e.dom.setStyle(a,"float","");const t=a.parentNode;be(e,t),e.selection.select(a.parentNode)}else e.selection.select(a),((e,t,a)=>{const i=()=>{a.onload=a.onerror=null,e.selection&&(e.selection.select(a),e.nodeChanged())};a.onload=()=>{t.width||t.height||!x(e)||e.dom.setAttribs(a,{width:String(a.clientWidth),height:String(a.clientHeight)}),i()},a.onerror=i})(e,t,a)})(e,s):((e,t)=>{if(t){const a=e.dom.is(t.parentNode,"figure.image")?t.parentNode:t;e.dom.remove(a),e.focus(),e.nodeChanged(),e.dom.isEmpty(e.getBody())&&(e.setContent(""),e.selection.setCursorLocation())}})(e,a)}else t.src&&ve(e,{src:"",alt:"",title:"",width:"",height:"",class:"",style:"",caption:!1,hspace:"",vspace:"",border:"",borderStyle:"",isDecorative:!1,...t})},fe=(we=(e,t)=>n(e)&&n(t)?fe(e,t):t,(...e)=>{if(0===e.length)throw new Error("Can't merge zero objects");const t={};for(let a=0;ar(e.value)?e.value:"",Ce=(e,t)=>{const a=[];return De.each(e,(e=>{const i=(e=>r(e.text)?e.text:r(e.title)?e.title:"")(e);if(void 0!==e.menu){const s=Ce(e.menu,t);a.push({text:i,items:s})}else{const s=t(e);a.push({text:i,value:s})}})),a},Ie=(e=_e)=>t=>t?h.from(t).map((t=>Ce(t,e))):h.none(),Ue=(e,t)=>(e=>{for(let i=0;iy(e,"items"))(a=e[i])?Ue(a.items,t):a.value===t?h.some(a):h.none();if(s.isSome())return s}var a;return h.none()})(e),Se=Ie,xe=(e,t)=>e.bind((e=>Ue(e,t))),Ne=e=>{const t=Se((t=>e.convertURL(t.value||t.url||"","src"))),a=new Promise((a=>{((e,t)=>{const a=R(e);r(a)?fetch(a).then((e=>{e.ok&&e.json().then(t)})):g(a)?a(t):t(a)})(e,(e=>{a(t(e).map((e=>w([[{text:"None",value:""}],e]))))}))})),i=(A=L(e),Ie(_e)(A)),s=N(e),o=T(e),n=(e=>U(e.options.get("images_upload_url")))(e),l=(e=>d(e.options.get("images_upload_handler")))(e),c=(e=>{const t=he(e);return t?ce((t=>pe(e,t)),t):{src:"",alt:"",title:"",width:"",height:"",class:"",style:"",caption:!1,hspace:"",vspace:"",border:"",borderStyle:"",isDecorative:!1}})(e),m=O(e),u=j(e),p=x(e),b=M(e),v=k(e),y=z(e),f=h.some(E(e)).filter((e=>r(e)&&e.length>0));var A;return a.then((e=>({image:c,imageList:e,classList:i,hasAdvTab:s,hasUploadTab:o,hasUploadUrl:n,hasUploadHandler:l,hasDescription:m,hasImageTitle:u,hasDimensions:p,hasImageCaption:b,prependURL:f,hasAccessibilityOptions:v,automaticUploads:y})))},Te=e=>{const t=e.imageList.map((e=>({name:"images",type:"listbox",label:"Image list",items:e}))),a={name:"alt",type:"input",label:"Alternative description",enabled:!(e.hasAccessibilityOptions&&e.image.isDecorative)},i=e.classList.map((e=>({name:"classes",type:"listbox",label:"Class",items:e})));return w([[{name:"src",type:"urlinput",filetype:"image",label:"Source",picker_text:"Browse files"}],t.toArray(),e.hasAccessibilityOptions&&e.hasDescription?[{type:"label",label:"Accessibility",items:[{name:"isDecorative",type:"checkbox",label:"Image is decorative"}]}]:[],e.hasDescription?[a]:[],e.hasImageTitle?[{name:"title",type:"input",label:"Image title"}]:[],e.hasDimensions?[{name:"dimensions",type:"sizeinput"}]:[],[{...(s=e.classList.isSome()&&e.hasImageCaption,s?{type:"grid",columns:2}:{type:"panel"}),items:w([i.toArray(),e.hasImageCaption?[{type:"label",label:"Caption",items:[{type:"checkbox",name:"caption",label:"Show caption"}]}]:[]])}]]);var s},Ee=e=>({title:"General",name:"general",items:Te(e)}),Le=Te,Oe=e=>({src:{value:e.src,meta:{}},images:e.src,alt:e.alt,title:e.title,dimensions:{width:e.width,height:e.height},classes:e.class,caption:e.caption,style:e.style,vspace:e.vspace,border:e.border,hspace:e.hspace,borderstyle:e.borderStyle,fileinput:[],isDecorative:e.isDecorative}),je=(e,t)=>({src:e.src.value,alt:null!==e.alt&&0!==e.alt.length||!t?e.alt:null,title:e.title,width:e.dimensions.width,height:e.dimensions.height,class:e.classes,style:e.style,caption:e.caption,hspace:e.hspace,vspace:e.vspace,border:e.border,borderStyle:e.borderstyle,isDecorative:e.isDecorative}),Me=(e,t,a,i)=>{((e,t)=>{const a=t.getData();((e,t)=>/^(?:[a-zA-Z]+:)?\/\//.test(t)?h.none():e.prependURL.bind((e=>t.substring(0,e.length)!==e?h.some(e+t):h.none())))(e,a.src.value).each((e=>{t.setData({src:{value:e,meta:a.src.meta}})}))})(t,i),((e,t)=>{const a=t.getData(),i=a.src.meta;if(void 0!==i){const s=fe({},a);((e,t,a)=>{e.hasDescription&&r(a.alt)&&(t.alt=a.alt),e.hasAccessibilityOptions&&(t.isDecorative=a.isDecorative||t.isDecorative||!1),e.hasImageTitle&&r(a.title)&&(t.title=a.title),e.hasDimensions&&(r(a.width)&&(t.dimensions.width=a.width),r(a.height)&&(t.dimensions.height=a.height)),r(a.class)&&xe(e.classList,a.class).each((e=>{t.classes=e.value})),e.hasImageCaption&&m(a.caption)&&(t.caption=a.caption),e.hasAdvTab&&(r(a.style)&&(t.style=a.style),r(a.vspace)&&(t.vspace=a.vspace),r(a.border)&&(t.border=a.border),r(a.hspace)&&(t.hspace=a.hspace),r(a.borderstyle)&&(t.borderstyle=a.borderstyle))})(e,s,i),t.setData(s)}})(t,i),((e,t,a,i)=>{const s=i.getData(),r=s.src.value,o=s.src.meta||{};o.width||o.height||!t.hasDimensions||(U(r)?e.imageSize(r).then((e=>{a.open&&i.setData({dimensions:e})})).catch((e=>console.error(e))):i.setData({dimensions:{width:"",height:""}}))})(e,t,a,i),((e,t,a)=>{const i=a.getData(),s=xe(e.imageList,i.src.value);t.prevImage=s,a.setData({images:s.map((e=>e.value)).getOr("")})})(t,a,i)},Re=(e,t,a,i)=>{const s=i.getData();var r;i.block("Uploading image"),(r=s.fileinput,(e=>0{i.unblock()}),(s=>{const r=URL.createObjectURL(s),o=()=>{i.unblock(),URL.revokeObjectURL(r)},n=s=>{i.setData({src:{value:s,meta:{}}}),i.showTab("general"),Me(e,t,a,i),i.focus("src")};var l;(l=s,new Promise(((e,t)=>{const a=new FileReader;a.onload=()=>{e(a.result)},a.onerror=()=>{var e;t(null===(e=a.error)||void 0===e?void 0:e.message)},a.readAsDataURL(l)}))).then((a=>{const l=e.createBlobCache(s,r,a);t.automaticUploads?e.uploadImage(l).then((e=>{n(e.url),o()})).catch((t=>{o(),e.alertErr(t,(()=>{i.focus("fileinput")}))})):(e.addToBlobCache(l),n(l.blobUri()),i.unblock())}))}))},ke=(e,t,a)=>(i,s)=>{"src"===s.name?Me(e,t,a,i):"images"===s.name?((e,t,a,i)=>{const s=i.getData(),r=xe(t.imageList,s.images);r.each((e=>{const t=""===s.alt||a.prevImage.map((e=>e.text===s.alt)).getOr(!1);t?""===e.value?i.setData({src:e,alt:a.prevAlt}):i.setData({src:e,alt:e.text}):i.setData({src:e})})),a.prevImage=r,Me(e,t,a,i)})(e,t,a,i):"alt"===s.name?a.prevAlt=i.getData().alt:"fileinput"===s.name?Re(e,t,a,i):"isDecorative"===s.name&&i.setEnabled("alt",!i.getData().isDecorative)},ze=e=>()=>{e.open=!1},Be=e=>e.hasAdvTab||e.hasUploadUrl||e.hasUploadHandler?{type:"tabpanel",tabs:w([[Ee(e)],e.hasAdvTab?[{title:"Advanced",name:"advanced",items:[{type:"grid",columns:2,items:[{type:"input",label:"Vertical space",name:"vspace",inputMode:"numeric"},{type:"input",label:"Horizontal space",name:"hspace",inputMode:"numeric"},{type:"input",label:"Border width",name:"border",inputMode:"numeric"},{type:"listbox",name:"borderstyle",label:"Border style",items:[{text:"Select...",value:""},{text:"Solid",value:"solid"},{text:"Dotted",value:"dotted"},{text:"Dashed",value:"dashed"},{text:"Double",value:"double"},{text:"Groove",value:"groove"},{text:"Ridge",value:"ridge"},{text:"Inset",value:"inset"},{text:"Outset",value:"outset"},{text:"None",value:"none"},{text:"Hidden",value:"hidden"}]}]}]}]:[],e.hasUploadTab&&(e.hasUploadUrl||e.hasUploadHandler)?[{title:"Upload",name:"upload",items:[{type:"dropzone",name:"fileinput"}]}]:[]])}:{type:"panel",items:Le(e)},Pe=(e,t,a)=>i=>{const s=fe(Oe(t.image),i.getData()),r={...s,style:le(a.normalizeCss,je(s,!1))};e.execCommand("mceUpdateImage",!1,je(r,t.hasAccessibilityOptions)),e.editorUpload.uploadImagesAuto(),i.close()},Fe=e=>t=>G(e,t)?(e=>new Promise((t=>{const a=document.createElement("img"),i=e=>{a.parentNode&&a.parentNode.removeChild(a),t(e)};a.addEventListener("load",(()=>{const e={width:B(a.width,a.clientWidth),height:B(a.height,a.clientHeight)};i(Promise.resolve(e))})),a.addEventListener("error",(()=>{i(Promise.reject(`Failed to get image dimensions for: ${e}`))}));const s=a.style;s.visibility="hidden",s.position="fixed",s.bottom=s.left="0px",s.width=s.height="auto",document.body.appendChild(a),a.src=e})))(e.documentBaseURI.toAbsolute(t)).then((e=>({width:String(e.width),height:String(e.height)}))):Promise.resolve({width:"",height:""}),He=e=>(t,a,i)=>{var s;return e.editorUpload.blobCache.create({blob:t,blobUri:a,name:null===(s=t.name)||void 0===s?void 0:s.replace(/\.[^\.]+$/,""),filename:t.name,base64:i.split(",")[1]})},Ge=e=>t=>{e.editorUpload.blobCache.add(t)},We=e=>(t,a)=>{e.windowManager.alert(t,a)},$e=e=>t=>pe(e,t),Ve=e=>t=>e.dom.parseStyle(t),Ke=e=>(t,a)=>e.dom.serializeStyle(t,a),Ze=e=>t=>Ae(e).upload([t],!1).then((e=>{var t;return 0===e.length?Promise.reject("Failed to upload image"):!1===e[0].status?Promise.reject(null===(t=e[0].error)||void 0===t?void 0:t.message):e[0]})),qe=e=>{const t={imageSize:Fe(e),addToBlobCache:Ge(e),createBlobCache:He(e),alertErr:We(e),normalizeCss:$e(e),parseStyle:Ve(e),serializeStyle:Ke(e),uploadImage:Ze(e)};return{open:()=>{Ne(e).then((a=>{const i=(e=>({prevImage:xe(e.imageList,e.image.src),prevAlt:e.image.alt,open:!0}))(a);return{title:"Insert/Edit Image",size:"normal",body:Be(a),buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:Oe(a.image),onSubmit:Pe(e,a,t),onChange:ke(t,a,i),onClose:ze(i)}})).then(e.windowManager.open)}}},Je=e=>{const t=e.attr("class");return d(t)&&/\bimage\b/.test(t)},Qe=e=>t=>{let a=t.length;const i=t=>{t.attr("contenteditable",e?"true":null)};for(;a--;){const s=t[a];Je(s)&&(s.attr("contenteditable",e?"false":null),De.each(s.getAll("figcaption"),i))}},Xe=e=>t=>{const a=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",a),a(),()=>{e.off("NodeChange",a)}};e.add("image",(e=>{(e=>{const t=e.options.register;t("image_dimensions",{processor:"boolean",default:!0}),t("image_advtab",{processor:"boolean",default:!1}),t("image_uploadtab",{processor:"boolean",default:!0}),t("image_prepend_url",{processor:"string",default:""}),t("image_class_list",{processor:"object[]"}),t("image_description",{processor:"boolean",default:!0}),t("image_title",{processor:"boolean",default:!1}),t("image_caption",{processor:"boolean",default:!1}),t("image_list",{processor:e=>{const t=!1===e||r(e)||((e,t)=>{if(l(e)){for(let a=0,i=e.length;a{e.on("PreInit",(()=>{e.parser.addNodeFilter("figure",Qe(!0)),e.serializer.addNodeFilter("figure",Qe(!1))}))})(e),(e=>{e.ui.registry.addToggleButton("image",{icon:"image",tooltip:"Insert/edit image",onAction:qe(e).open,onSetup:t=>{t.setActive(d(he(e)));const a=e.selection.selectorChangedWithUnbind("img:not([data-mce-object]):not([data-mce-placeholder]),figure.image",t.setActive).unbind,i=Xe(e)(t);return()=>{a(),i()}}}),e.ui.registry.addMenuItem("image",{icon:"image",text:"Image...",onAction:qe(e).open,onSetup:Xe(e)}),e.ui.registry.addContextMenu("image",{update:t=>e.selection.isEditable()&&(re(t)||"IMG"===t.nodeName&&!H(t))?["image"]:[]})})(e),(e=>{e.addCommand("mceImage",qe(e).open),e.addCommand("mceUpdateImage",((t,a)=>{e.undoManager.transact((()=>ye(e,a)))}))})(e)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/importcss/plugin.min.js b/src/main/webapp/content/tinymce/plugins/importcss/plugin.min.js new file mode 100644 index 0000000..b1b1a5e --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/importcss/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(s=r=e,(o=String).prototype.isPrototypeOf(s)||(null===(n=r.constructor)||void 0===n?void 0:n.name)===o.name)?"string":t;var s,r,o,n})(t)===e,s=t("string"),r=t("object"),o=t("array"),n=e=>"function"==typeof e;var c=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),i=tinymce.util.Tools.resolve("tinymce.EditorManager"),l=tinymce.util.Tools.resolve("tinymce.Env"),a=tinymce.util.Tools.resolve("tinymce.util.Tools");const p=e=>t=>t.options.get(e),u=p("importcss_merge_classes"),m=p("importcss_exclusive"),f=p("importcss_selector_converter"),y=p("importcss_selector_filter"),d=p("importcss_groups"),h=p("importcss_append"),g=p("importcss_file_filter"),_=p("skin"),v=p("skin_url"),b=Array.prototype.push,x=/^\.(?:ephox|tiny-pageembed|mce)(?:[.-]+\w+)+$/,T=e=>s(e)?t=>-1!==t.indexOf(e):e instanceof RegExp?t=>e.test(t):e,S=(e,t)=>{let s={};const r=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(t);if(!r)return;const o=r[1],n=r[2].substr(1).split(".").join(" "),c=a.makeMap("a,img");return r[1]?(s={title:t},e.schema.getTextBlockElements()[o]?s.block=o:e.schema.getBlockElements()[o]||c[o.toLowerCase()]?s.selector=o:s.inline=o):r[2]&&(s={inline:"span",title:t.substr(1),classes:n}),u(e)?s.classes=n:s.attributes={class:n},s},k=(e,t)=>null===t||m(e),M=e=>{e.on("init",(()=>{const t=(()=>{const e=[],t=[],s={};return{addItemToGroup:(e,r)=>{s[e]?s[e].push(r):(t.push(e),s[e]=[r])},addItem:t=>{e.push(t)},toFormats:()=>{return(r=t,n=e=>{const t=s[e];return 0===t.length?[]:[{title:e,items:t}]},(e=>{const t=[];for(let s=0,r=e.length;s{const s=e.length,r=new Array(s);for(let o=0;oa.map(e,(e=>a.extend({},e,{original:e,selectors:{},filter:T(e.filter)}))))(d(e)),u=(t,s)=>{if(((e,t,s,r)=>!(k(e,s)?t in r:t in s.selectors))(e,t,s,r)){((e,t,s,r)=>{k(e,s)?r[t]=!0:s.selectors[t]=!0})(e,t,s,r);const o=((e,t,s,r)=>{let o;const n=f(e);return o=r&&r.selector_converter?r.selector_converter:n||(()=>S(e,s)),o.call(t,s,r)})(e,e.plugins.importcss,t,s);if(o){const t=o.name||c.DOM.uniqueId();return e.formatter.register(t,o),{title:o.title,format:t}}}return null};a.each(((e,t,r)=>{const o=[],n={},c=(t,n)=>{let p,u=t.href;if(u=(e=>{const t=l.cacheSuffix;return s(e)&&(e=e.replace("?"+t,"").replace("&"+t,"")),e})(u),u&&(!r||r(u,n))&&!((e,t)=>{const s=_(e);if(s){const r=v(e),o=r?e.documentBaseURI.toAbsolute(r):i.baseURL+"/skins/ui/"+s,n=i.baseURL+"/skins/content/",c=e.editorManager.suffix;return t===o+"/content"+(e.inline?".inline":"")+`${c}.css`||-1!==t.indexOf(n)}return!1})(e,u)){a.each(t.imports,(e=>{c(e,!0)}));try{p=t.cssRules||t.rules}catch(e){}a.each(p,(e=>{e.styleSheet&&e.styleSheet?c(e.styleSheet,!0):e.selectorText&&a.each(e.selectorText.split(","),(e=>{o.push(a.trim(e))}))}))}};a.each(e.contentCSS,(e=>{n[e]=!0})),r||(r=(e,t)=>t||n[e]);try{a.each(t.styleSheets,(e=>{c(e)}))}catch(e){}return o})(e,e.getDoc(),T(g(e))),(e=>{if(!x.test(e)&&(!n||n(e))){const s=((e,t)=>a.grep(e,(e=>!e.filter||e.filter(t))))(p,e);if(s.length>0)a.each(s,(s=>{const r=u(e,s);r&&t.addItemToGroup(s.title,r)}));else{const s=u(e,null);s&&t.addItem(s)}}}));const m=t.toFormats();e.dispatch("addStyleModifications",{items:m,replace:!h(e)})}))};e.add("importcss",(e=>((e=>{const t=e.options.register,o=e=>s(e)||n(e)||r(e);t("importcss_merge_classes",{processor:"boolean",default:!0}),t("importcss_exclusive",{processor:"boolean",default:!0}),t("importcss_selector_converter",{processor:"function"}),t("importcss_selector_filter",{processor:o}),t("importcss_file_filter",{processor:o}),t("importcss_groups",{processor:"object[]"}),t("importcss_append",{processor:"boolean",default:!1})})(e),M(e),(e=>({convertSelectorToFormat:t=>S(e,t)}))(e))))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/insertdatetime/plugin.min.js b/src/main/webapp/content/tinymce/plugins/insertdatetime/plugin.min.js new file mode 100644 index 0000000..8a05382 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/insertdatetime/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),a=t("insertdatetime_dateformat"),n=t("insertdatetime_timeformat"),r=t("insertdatetime_formats"),s=t("insertdatetime_element"),i="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),o="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),l="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),m="January February March April May June July August September October November December".split(" "),c=(e,t)=>{if((e=""+e).length(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("%D","%m/%d/%Y")).replace("%r","%I:%M:%S %p")).replace("%Y",""+a.getFullYear())).replace("%y",""+a.getYear())).replace("%m",c(a.getMonth()+1,2))).replace("%d",c(a.getDate(),2))).replace("%H",""+c(a.getHours(),2))).replace("%M",""+c(a.getMinutes(),2))).replace("%S",""+c(a.getSeconds(),2))).replace("%I",""+((a.getHours()+11)%12+1))).replace("%p",a.getHours()<12?"AM":"PM")).replace("%B",""+e.translate(m[a.getMonth()]))).replace("%b",""+e.translate(l[a.getMonth()]))).replace("%A",""+e.translate(o[a.getDay()]))).replace("%a",""+e.translate(i[a.getDay()]))).replace("%%","%"),u=(e,t)=>{if(s(e)&&e.selection.isEditable()){const a=d(e,t);let n;n=/%[HMSIp]/.test(t)?d(e,"%Y-%m-%dT%H:%M"):d(e,"%Y-%m-%d");const r=e.dom.getParent(e.selection.getStart(),"time");r?((e,t,a,n)=>{const r=e.dom.create("time",{datetime:a},n);e.dom.replace(r,t),e.selection.select(r,!0),e.selection.collapse(!1)})(e,r,n,a):e.insertContent('")}else e.insertContent(d(e,t))};var p=tinymce.util.Tools.resolve("tinymce.util.Tools");const g=e=>t=>{const a=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",a),a(),()=>{e.off("NodeChange",a)}};e.add("insertdatetime",(e=>{(e=>{const t=e.options.register;t("insertdatetime_dateformat",{processor:"string",default:e.translate("%Y-%m-%d")}),t("insertdatetime_timeformat",{processor:"string",default:e.translate("%H:%M:%S")}),t("insertdatetime_formats",{processor:"string[]",default:["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"]}),t("insertdatetime_element",{processor:"boolean",default:!1})})(e),(e=>{e.addCommand("mceInsertDate",((t,n)=>{u(e,null!=n?n:a(e))})),e.addCommand("mceInsertTime",((t,a)=>{u(e,null!=a?a:n(e))}))})(e),(e=>{const t=r(e),a=(e=>{let t=e;return{get:()=>t,set:e=>{t=e}}})((e=>{const t=r(e);return t.length>0?t[0]:n(e)})(e)),s=t=>e.execCommand("mceInsertDate",!1,t);e.ui.registry.addSplitButton("insertdatetime",{icon:"insert-time",tooltip:"Insert date/time",select:e=>e===a.get(),fetch:a=>{a(p.map(t,(t=>({type:"choiceitem",text:d(e,t),value:t}))))},onAction:e=>{s(a.get())},onItemAction:(e,t)=>{a.set(t),s(t)},onSetup:g(e)});const i=e=>()=>{a.set(e),s(e)};e.ui.registry.addNestedMenuItem("insertdatetime",{icon:"insert-time",text:"Date/time",getSubmenuItems:()=>p.map(t,(t=>({type:"menuitem",text:d(e,t),onAction:i(t)}))),onSetup:g(e)})})(e)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/link/plugin.min.js b/src/main/webapp/content/tinymce/plugins/link/plugin.min.js new file mode 100644 index 0000000..d8c40c7 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/link/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(l=o.constructor)||void 0===l?void 0:l.name)===r.name)?"string":t;var n,o,r,l})(t)===e,n=e=>t=>typeof t===e,o=t("string"),r=t("object"),l=t("array"),s=e=>null===e;const i=n("boolean"),a=e=>!(e=>null==e)(e),c=n("function"),u=(e,t)=>{if(l(e)){for(let n=0,o=e.length;n{},d=(e,t)=>e===t;class m{constructor(e,t){this.tag=e,this.value=t}static some(e){return new m(!0,e)}static none(){return m.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?m.some(e(this.value)):m.none()}bind(e){return this.tag?e(this.value):m.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:m.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return a(e)?m.some(e):m.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}m.singletonNone=new m(!1);const h=Array.prototype.indexOf,p=Array.prototype.push,f=e=>{const t=[];for(let n=0,o=e.length;n{for(let n=0;ne.exists((e=>n(e,t))),b=e=>{const t=[],n=e=>{t.push(e)};for(let t=0;te?m.some(t):m.none(),y=e=>t=>t.options.get(e),_=y("link_assume_external_targets"),w=y("link_context_toolbar"),C=y("link_list"),O=y("link_default_target"),S=y("link_default_protocol"),A=y("link_target_list"),N=y("link_rel_list"),E=y("link_class_list"),R=y("link_title"),T=y("allow_unsafe_link_target"),L=y("link_quicklink"),P=y("link_attributes_postprocess"),M=Object.keys,D=Object.hasOwnProperty,B=(e,t)=>D.call(e,t);var I=tinymce.util.Tools.resolve("tinymce.util.URI"),K=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),j=tinymce.util.Tools.resolve("tinymce.util.Tools");const U=e=>a(e)&&"a"===e.nodeName.toLowerCase(),q=e=>U(e)&&!!$(e),F=(e,t)=>{if(e.collapsed)return[];{const n=e.cloneContents(),o=n.firstChild,r=new K(o,n),l=[];let s=o;do{t(s)&&l.push(s)}while(s=r.next());return l}},V=e=>/^\w+:/i.test(e),$=e=>{var t,n;return null!==(n=null!==(t=e.getAttribute("data-mce-href"))&&void 0!==t?t:e.getAttribute("href"))&&void 0!==n?n:""},z=(e,t)=>{const n=["noopener"],o=e?e.split(/\s+/):[],r=e=>e.filter((e=>-1===j.inArray(n,e))),l=t?(e=>(e=r(e)).length>0?e.concat(n):n)(o):r(o);return l.length>0?(e=>j.trim(e.sort().join(" ")))(l):""},G=(e,t)=>(t=t||W(e.selection.getRng())[0]||e.selection.getNode(),Z(t)?m.from(e.dom.select("a[href]",t)[0]):m.from(e.dom.getParent(t,"a[href]"))),H=(e,t)=>G(e,t).isSome(),J=(e,t)=>t.fold((()=>e.getContent({format:"text"})),(e=>e.innerText||e.textContent||"")).replace(/\uFEFF/g,""),W=e=>F(e,q),Q=e=>j.grep(e,q),X=e=>Q(e).length>0,Y=e=>{const t=e.schema.getTextInlineElements();if(G(e).exists((e=>e.hasAttribute("data-mce-block"))))return!1;const n=e.selection.getRng();return!!n.collapsed||0===F(n,(e=>1===e.nodeType&&!U(e)&&!B(t,e.nodeName.toLowerCase()))).length},Z=e=>a(e)&&"FIGURE"===e.nodeName&&/\bimage\b/i.test(e.className),ee=(e,t,n)=>{const o=e.selection.getNode(),r=G(e,o),l=((e,t)=>{const n={...t};if(0===N(e).length&&!T(e)){const e=z(n.rel,"_blank"===n.target);n.rel=e||null}return m.from(n.target).isNone()&&!1===A(e)&&(n.target=O(e)),n.href=((e,t)=>"http"!==t&&"https"!==t||V(e)?e:t+"://"+e)(n.href,_(e)),n})(e,(e=>{return t=["title","rel","class","target"],n=(t,n)=>(e[n].each((e=>{t[n]=e.length>0?e:null})),t),o={href:e.href},((e,t)=>{for(let n=0,o=e.length;n{o=n(o,e)})),o;var t,n,o})(n)),s=P(e);a(s)&&s(l),e.undoManager.transact((()=>{n.href===t.href&&t.attach(),r.fold((()=>{((e,t,n,o)=>{const r=e.dom;Z(t)?le(r,t,o):n.fold((()=>{e.execCommand("mceInsertLink",!1,o);const t=e.selection.getEnd(),n=r.createRng();n.setStartAfter(t),n.setEndAfter(t),e.selection.setRng(n)}),(t=>{e.insertContent(r.createHTML("a",o,r.encode(t)))}))})(e,o,n.text,l)}),(t=>{e.focus(),((e,t,n,o)=>{n.each((e=>{B(t,"innerText")?t.innerText=e:t.textContent=e})),e.dom.setAttribs(t,o);const r=e.dom.createRng();r.setStartAfter(t),r.setEndAfter(t),e.selection.setRng(r)})(e,t,n.text,l)}))}))},te=e=>{const{class:t,href:n,rel:o,target:r,text:l,title:i}=e;return(e=>{const t={};var n;return((e,t,n,o)=>{((e,t)=>{const n=M(e);for(let o=0,r=n.length;o{(t(e,r)?n:o)(e,r)}))})(e,((e,t)=>!1===s(e)),(n=t,(e,t)=>{n[t]=e}),g),t})({class:t.getOrNull(),href:n,rel:o.getOrNull(),target:r.getOrNull(),text:l.getOrNull(),title:i.getOrNull()})},ne=(e,t,n)=>{const o=((e,t)=>{const n=e.options.get,o={allow_html_data_urls:n("allow_html_data_urls"),allow_script_urls:n("allow_script_urls"),allow_svg_data_urls:n("allow_svg_data_urls")},r=t.href;return{...t,href:I.isDomSafe(r,"a",o)?r:""}})(e,n);e.hasPlugin("rtc",!0)?e.execCommand("createlink",!1,te(o)):ee(e,t,o)},oe=e=>{e.hasPlugin("rtc",!0)?e.execCommand("unlink"):(e=>{e.undoManager.transact((()=>{const t=e.selection.getNode();Z(t)?re(e,t):(e=>{const t=e.dom,n=e.selection,o=n.getBookmark(),r=n.getRng().cloneRange(),l=t.getParent(r.startContainer,"a[href]",e.getBody()),s=t.getParent(r.endContainer,"a[href]",e.getBody());l&&r.setStartBefore(l),s&&r.setEndAfter(s),n.setRng(r),e.execCommand("unlink"),n.moveToBookmark(o)})(e),e.focus()}))})(e)},re=(e,t)=>{var n;const o=e.dom.select("img",t)[0];if(o){const r=e.dom.getParents(o,"a[href]",t)[0];r&&(null===(n=r.parentNode)||void 0===n||n.insertBefore(o,r),e.dom.remove(r))}},le=(e,t,n)=>{var o;const r=e.select("img",t)[0];if(r){const t=e.create("a",n);null===(o=r.parentNode)||void 0===o||o.insertBefore(t,r),t.appendChild(r)}},se=e=>o(e.value)?e.value:"",ie=(e,t)=>{const n=[];return j.each(e,(e=>{const r=(e=>o(e.text)?e.text:o(e.title)?e.title:"")(e);if(void 0!==e.menu){const o=ie(e.menu,t);n.push({text:r,items:o})}else{const o=t(e);n.push({text:r,value:o})}})),n},ae=(e=se)=>t=>m.from(t).map((t=>ie(t,e))),ce=e=>ae(se)(e),ue=ae,ge=(e,t)=>n=>({name:e,type:"listbox",label:t,items:n}),de=se,me=(e,t)=>k(t,(t=>(e=>{return B(t=e,n="items")&&void 0!==t[n]&&null!==t[n];var t,n})(t)?me(e,t.items):x(t.value===e,t))),he=(e,t)=>{const n={text:e.text,title:e.title},o=(e,o)=>{const r=(l=t,s=o,"link"===s?l.link:"anchor"===s?l.anchor:m.none()).getOr([]);var l,s;return((e,t,n,o)=>{const r=o[t],l=e.length>0;return void 0!==r?me(r,n).map((t=>({url:{value:t.value,meta:{text:l?e:t.text,attach:g}},text:l?e:t.text}))):m.none()})(n.text,o,r,e)};return{onChange:(e,t)=>{const r=t.name;return"url"===r?(e=>{const t=(o=e.url,x(n.text.length<=0,m.from(null===(r=o.meta)||void 0===r?void 0:r.text).getOr(o.value)));var o,r;const l=(e=>{var t;return x(n.title.length<=0,m.from(null===(t=e.meta)||void 0===t?void 0:t.title).getOr(""))})(e.url);return t.isSome()||l.isSome()?m.some({...t.map((e=>({text:e}))).getOr({}),...l.map((e=>({title:e}))).getOr({})}):m.none()})(e()):((e,t)=>h.call(e,t))(["anchor","link"],r)>-1?o(e(),r):"text"===r||"title"===r?(n[r]=e()[r],m.none()):m.none()}}};var pe=tinymce.util.Tools.resolve("tinymce.util.Delay");const fe=e=>{const t=e.href;return t.indexOf("@")>0&&-1===t.indexOf("/")&&-1===t.indexOf("mailto:")?m.some({message:"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?",preprocess:e=>({...e,href:"mailto:"+t})}):m.none()},ke=(e,t)=>n=>{const o=n.href;return 1===e&&!V(o)||0===e&&/^\s*www(\.|\d\.)/i.test(o)?m.some({message:`The URL you entered seems to be an external link. Do you want to add the required ${t}:// prefix?`,preprocess:e=>({...e,href:t+"://"+o})}):m.none()},ve=e=>{const t=e.dom.select("a:not([href])"),n=f(((e,t)=>{const n=e.length,o=new Array(n);for(let r=0;r{const t=e.name||e.id;return t?[{text:t,value:"#"+t}]:[]})));return n.length>0?m.some([{text:"None",value:""}].concat(n)):m.none()},be=e=>{const t=E(e);return t.length>0?ce(t):m.none()},xe=e=>{try{return m.some(JSON.parse(e))}catch(e){return m.none()}},ye=(e,t)=>{const n=N(e);if(n.length>0){const o=v(t,"_blank"),r=e=>z(de(e),o);return(!1===T(e)?ue(r):ce)(n)}return m.none()},_e=[{text:"Current window",value:""},{text:"New window",value:"_blank"}],we=e=>{const t=A(e);return l(t)?ce(t).orThunk((()=>m.some(_e))):!1===t?m.none():m.some(_e)},Ce=(e,t,n)=>{const o=e.getAttrib(t,n);return null!==o&&o.length>0?m.some(o):m.none()},Oe=(e,t)=>(e=>{const t=t=>e.convertURL(t.value||t.url||"","href"),n=C(e);return new Promise((e=>{o(n)?fetch(n).then((e=>e.ok?e.text().then(xe):Promise.reject())).then(e,(()=>e(m.none()))):c(n)?n((t=>e(m.some(t)))):e(m.from(n))})).then((e=>e.bind(ue(t)).map((e=>e.length>0?[{text:"None",value:""}].concat(e):e))))})(e).then((n=>{const o=((e,t)=>{const n=e.dom,o=Y(e)?m.some(J(e.selection,t)):m.none(),r=t.bind((e=>m.from(n.getAttrib(e,"href")))),l=t.bind((e=>m.from(n.getAttrib(e,"target")))),s=t.bind((e=>Ce(n,e,"rel"))),i=t.bind((e=>Ce(n,e,"class")));return{url:r,text:o,title:t.bind((e=>Ce(n,e,"title"))),target:l,rel:s,linkClass:i}})(e,t);return{anchor:o,catalogs:{targets:we(e),rels:ye(e,o.target),classes:be(e),anchor:ve(e),link:n},optNode:t,flags:{titleEnabled:R(e)}}})),Se=e=>{const t=(e=>{const t=G(e);return Oe(e,t)})(e);t.then((t=>{const n=((e,t)=>n=>{const o=n.getData();if(!o.url.value)return oe(e),void n.close();const r=e=>m.from(o[e]).filter((n=>!v(t.anchor[e],n))),l={href:o.url.value,text:r("text"),target:r("target"),rel:r("rel"),class:r("linkClass"),title:r("title")},s={href:o.url.value,attach:void 0!==o.url.meta&&o.url.meta.attach?o.url.meta.attach:g};((e,t)=>k([fe,ke(_(e),S(e))],(e=>e(t))).fold((()=>Promise.resolve(t)),(n=>new Promise((o=>{((e,t,n)=>{const o=e.selection.getRng();pe.setEditorTimeout(e,(()=>{e.windowManager.confirm(t,(t=>{e.selection.setRng(o),n(t)}))}))})(e,n.message,(e=>{o(e?n.preprocess(t):t)}))})))))(e,l).then((t=>{ne(e,s,t)})),n.close()})(e,t);return((e,t,n)=>{const o=e.anchor.text.map((()=>({name:"text",type:"input",label:"Text to display"}))).toArray(),r=e.flags.titleEnabled?[{name:"title",type:"input",label:"Title"}]:[],l=((e,t)=>{const n=e.anchor,o=n.url.getOr("");return{url:{value:o,meta:{original:{value:o}}},text:n.text.getOr(""),title:n.title.getOr(""),anchor:o,link:o,rel:n.rel.getOr(""),target:n.target.or(t).getOr(""),linkClass:n.linkClass.getOr("")}})(e,m.from(O(n))),s=e.catalogs,i=he(l,s);return{title:"Insert/Edit Link",size:"normal",body:{type:"panel",items:f([[{name:"url",type:"urlinput",filetype:"file",label:"URL",picker_text:"Browse links"}],o,r,b([s.anchor.map(ge("anchor","Anchors")),s.rels.map(ge("rel","Rel")),s.targets.map(ge("target","Open link in...")),s.link.map(ge("link","Link list")),s.classes.map(ge("linkClass","Class"))])])},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:l,onChange:(e,{name:t})=>{i.onChange(e.getData,{name:t}).each((t=>{e.setData(t)}))},onSubmit:t}})(t,n,e)})).then((t=>{e.windowManager.open(t)}))};var Ae=tinymce.util.Tools.resolve("tinymce.util.VK");const Ne=(e,t)=>{if(t){const o=$(t);if(/^#/.test(o)){const t=e.dom.select(`${o},[name="${n=o,((e,t)=>((e,t)=>""===t||e.length>=t.length&&e.substr(0,0+t.length)===t)(e,t))(n,"#")?(e=>e.substring(1))(n):n}"]`);t.length&&e.selection.scrollIntoView(t[0],!0)}else(e=>{const t=document.createElement("a");t.target="_blank",t.href=e,t.rel="noreferrer noopener";const n=new MouseEvent("click",{bubbles:!0,cancelable:!0,view:window});document.dispatchEvent(n),((e,t)=>{document.body.appendChild(e),e.dispatchEvent(t),document.body.removeChild(e)})(t,n)})(t.href)}var n},Ee=(e,t)=>{const n=Q(e.dom.getParents(t));return x(1===n.length,n[0])},Re=e=>e.selection.isCollapsed()||(e=>{const t=e.selection.getRng(),n=t.startContainer;return q(n)&&t.startContainer===t.endContainer&&1===e.dom.select("img",n).length})(e)?Ee(e,e.selection.getStart()):(e=>{const t=W(e.selection.getRng());return x(t.length>0,t[0]).or(Ee(e,e.selection.getNode()))})(e),Te=e=>()=>{e.execCommand("mceLink",!1,{dialog:!0})},Le=(e,t)=>(e.on("NodeChange",t),()=>e.off("NodeChange",t)),Pe=e=>t=>{const n=()=>{t.setActive(!e.mode.isReadOnly()&&H(e,e.selection.getNode())),t.setEnabled(e.selection.isEditable())};return n(),Le(e,n)},Me=e=>t=>{const n=()=>{t.setEnabled(e.selection.isEditable())};return n(),Le(e,n)},De=e=>t=>{const n=e.dom.getParents(e.selection.getStart()),o=n=>{t.setEnabled((t=>{return X(t)||(n=e.selection.getRng(),W(n).length>0);var n})(n)&&e.selection.isEditable())};return o(n),Le(e,(e=>o(e.parents)))},Be=e=>{const t=(e=>{const t=(()=>{const e=(e=>{const t=(e=>{let t=e;return{get:()=>t,set:e=>{t=e}}})(m.none()),n=()=>t.get().each(e);return{clear:()=>{n(),t.set(m.none())},isSet:()=>t.get().isSome(),get:()=>t.get(),set:e=>{n(),t.set(m.some(e))}}})(g);return{...e,on:t=>e.get().each(t)}})(),n=()=>t.get().or(Re(e));return e.on("contextmenu",(n=>{Ee(e,n.target).each(t.set)})),e.on("SelectionChange",(()=>{t.isSet()||Re(e).each(t.set)})),e.on("click",(n=>{t.clear();const o=Q(e.dom.getParents(n.target));1===o.length&&Ae.metaKeyPressed(n)&&(n.preventDefault(),Ne(e,o[0]))})),e.on("keydown",(o=>{t.clear(),!o.isDefaultPrevented()&&13===o.keyCode&&(e=>!0===e.altKey&&!1===e.shiftKey&&!1===e.ctrlKey&&!1===e.metaKey)(o)&&n().each((t=>{o.preventDefault(),Ne(e,t)}))})),{gotoSelectedLink:()=>n().each((t=>Ne(e,t)))}})(e);((e,t)=>{e.ui.registry.addToggleButton("link",{icon:"link",tooltip:"Insert/edit link",shortcut:"Meta+K",onAction:Te(e),onSetup:Pe(e)}),e.ui.registry.addButton("openlink",{icon:"new-tab",tooltip:"Open link",onAction:t.gotoSelectedLink,onSetup:De(e)}),e.ui.registry.addButton("unlink",{icon:"unlink",tooltip:"Remove link",onAction:()=>oe(e),onSetup:De(e)})})(e,t),((e,t)=>{e.ui.registry.addMenuItem("openlink",{text:"Open link",icon:"new-tab",onAction:t.gotoSelectedLink,onSetup:De(e)}),e.ui.registry.addMenuItem("link",{icon:"link",text:"Link...",shortcut:"Meta+K",onAction:Te(e),onSetup:Me(e)}),e.ui.registry.addMenuItem("unlink",{icon:"unlink",text:"Remove link",onAction:()=>oe(e),onSetup:De(e)})})(e,t),(e=>{e.ui.registry.addContextMenu("link",{update:t=>e.dom.isEditable(t)?X(e.dom.getParents(t,"a"))?"link unlink openlink":"link":""})})(e),((e,t)=>{const n=t=>{const n=e.selection.getNode();return t.setEnabled(H(e,n)&&e.selection.isEditable()),g};e.ui.registry.addContextForm("quicklink",{launch:{type:"contextformtogglebutton",icon:"link",tooltip:"Link",onSetup:Pe(e)},label:"Link",predicate:t=>w(e)&&H(e,t),initValue:()=>G(e).fold((()=>""),$),commands:[{type:"contextformtogglebutton",icon:"link",tooltip:"Link",primary:!0,onSetup:t=>{const n=e.selection.getNode();return t.setActive(H(e,n)),Pe(e)(t)},onAction:t=>{const n=t.getValue(),o=(t=>{const n=G(e),o=Y(e);if(n.isNone()&&o){const o=J(e.selection,n);return x(0===o.length,t)}return m.none()})(n);ne(e,{href:n,attach:g},{href:n,text:o,title:m.none(),rel:m.none(),target:m.from(O(e)),class:m.none()}),(e=>{e.selection.collapse(!1)})(e),t.hide()}},{type:"contextformbutton",icon:"unlink",tooltip:"Remove link",onSetup:n,onAction:t=>{oe(e),t.hide()}},{type:"contextformbutton",icon:"new-tab",tooltip:"Open link",onSetup:n,onAction:e=>{t.gotoSelectedLink(),e.hide()}}]})})(e,t)};e.add("link",(e=>{(e=>{const t=e.options.register;t("link_assume_external_targets",{processor:e=>{const t=o(e)||i(e);return t?!0===e?{value:1,valid:t}:"http"===e||"https"===e?{value:e,valid:t}:{value:0,valid:t}:{valid:!1,message:"Must be a string or a boolean."}},default:!1}),t("link_context_toolbar",{processor:"boolean",default:!1}),t("link_list",{processor:e=>o(e)||c(e)||u(e,r)}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"}),t("link_target_list",{processor:e=>i(e)||u(e,r),default:!0}),t("link_rel_list",{processor:"object[]",default:[]}),t("link_class_list",{processor:"object[]",default:[]}),t("link_title",{processor:"boolean",default:!0}),t("allow_unsafe_link_target",{processor:"boolean",default:!1}),t("link_quicklink",{processor:"boolean",default:!1}),t("link_attributes_postprocess",{processor:"function"})})(e),(e=>{e.addCommand("mceLink",((t,n)=>{!0!==(null==n?void 0:n.dialog)&&L(e)?e.dispatch("contexttoolbar-show",{toolbarKey:"quicklink"}):Se(e)}))})(e),Be(e),(e=>{e.addShortcut("Meta+K","",(()=>{e.execCommand("mceLink")}))})(e)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/lists/plugin.min.js b/src/main/webapp/content/tinymce/plugins/lists/plugin.min.js new file mode 100644 index 0000000..ba49b0b --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/lists/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(s=o.constructor)||void 0===s?void 0:s.name)===r.name)?"string":t;var n,o,r,s})(t)===e,n=e=>t=>typeof t===e,o=t("string"),r=t("object"),s=t("array"),i=n("boolean"),l=e=>!(e=>null==e)(e),a=n("function"),d=n("number"),c=()=>{},m=e=>()=>e,u=(e,t)=>e===t,p=e=>t=>!e(t),g=m(!1);class h{constructor(e,t){this.tag=e,this.value=t}static some(e){return new h(!0,e)}static none(){return h.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?h.some(e(this.value)):h.none()}bind(e){return this.tag?e(this.value):h.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:h.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return l(e)?h.some(e):h.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}h.singletonNone=new h(!1);const f=Array.prototype.slice,y=Array.prototype.indexOf,v=Array.prototype.push,C=(e,t)=>{return n=e,o=t,y.call(n,o)>-1;var n,o},b=(e,t)=>{for(let n=0,o=e.length;n{const n=e.length,o=new Array(n);for(let r=0;r{for(let n=0,o=e.length;n{const n=[];for(let o=0,r=e.length;o(S(e,((e,o)=>{n=t(n,e,o)})),n),A=(e,t,n)=>{for(let o=0,r=e.length;oA(e,t,g),x=(e,t)=>(e=>{const t=[];for(let n=0,o=e.length;n{const t=f.call(e,0);return t.reverse(),t},E=(e,t)=>t>=0&&tE(e,0),D=e=>E(e,e.length-1),B=(e,t)=>{const n=[],o=a(t)?e=>b(n,(n=>t(n,e))):e=>C(n,e);for(let t=0,r=e.length;te.exists((e=>n(e,t))),P=(e,t,n)=>e.isSome()&&t.isSome()?h.some(n(e.getOrDie(),t.getOrDie())):h.none(),I=e=>{if(null==e)throw new Error("Node cannot be null or undefined");return{dom:e}},R=(e,t)=>{const n=(t||document).createElement("div");if(n.innerHTML=e,!n.hasChildNodes()||n.childNodes.length>1){const t="HTML does not have a single root node";throw console.error(t,e),new Error(t)}return I(n.childNodes[0])},U=(e,t)=>{const n=(t||document).createElement(e);return I(n)},$=I,_=(e,t)=>{const n=e.dom;if(1!==n.nodeType)return!1;{const e=n;if(void 0!==e.matches)return e.matches(t);if(void 0!==e.msMatchesSelector)return e.msMatchesSelector(t);if(void 0!==e.webkitMatchesSelector)return e.webkitMatchesSelector(t);if(void 0!==e.mozMatchesSelector)return e.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")}},H=(e,t)=>e.dom===t.dom,F=_,V="undefined"!=typeof window?window:Function("return this;")(),j=(e,t)=>((e,t)=>{let n=null!=t?t:V;for(let t=0;t{const t=j("ownerDocument.defaultView",e);return r(e)&&((e=>((e,t)=>{const n=((e,t)=>j(e,t))(e,t);if(null==n)throw new Error(e+" not available on this browser");return n})("HTMLElement",e))(t).prototype.isPrototypeOf(e)||/^HTML\w*Element$/.test(K(e).constructor.name))},Q=e=>e.dom.nodeName.toLowerCase(),W=e=>e.dom.nodeType,q=e=>t=>W(t)===e,Z=e=>G(e)&&z(e.dom),G=q(1),J=q(3),X=q(11),Y=e=>t=>G(t)&&Q(t)===e,ee=e=>h.from(e.dom.parentNode).map($),te=e=>N(e.dom.childNodes,$),ne=(e,t)=>{const n=e.dom.childNodes;return h.from(n[t]).map($)},oe=e=>ne(e,0),re=e=>ne(e,e.dom.childNodes.length-1),se=e=>$(e.dom.host),ie=e=>{const t=J(e)?e.dom.parentNode:e.dom;if(null==t||null===t.ownerDocument)return!1;const n=t.ownerDocument;return(e=>{const t=(e=>$(e.dom.getRootNode()))(e);return X(n=t)&&l(n.dom.host)?h.some(t):h.none();var n})($(t)).fold((()=>n.body.contains(t)),(o=ie,r=se,e=>o(r(e))));var o,r};var le=(e,t,n,o,r)=>e(n,o)?h.some(n):a(r)&&r(n)?h.none():t(n,o,r);const ae=(e,t,n)=>{let o=e.dom;const r=a(n)?n:g;for(;o.parentNode;){o=o.parentNode;const e=$(o);if(t(e))return h.some(e);if(r(e))break}return h.none()},de=(e,t,n)=>le(((e,t)=>t(e)),ae,e,t,n),ce=(e,t,n)=>ae(e,(e=>_(e,t)),n),me=(e,t)=>{ee(e).each((n=>{n.dom.insertBefore(t.dom,e.dom)}))},ue=(e,t)=>{e.dom.appendChild(t.dom)},pe=(e,t)=>{S(t,(t=>{ue(e,t)}))},ge=e=>{e.dom.textContent="",S(te(e),(e=>{he(e)}))},he=e=>{const t=e.dom;null!==t.parentNode&&t.parentNode.removeChild(t)};var fe=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),ye=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),ve=tinymce.util.Tools.resolve("tinymce.util.VK");const Ce=e=>N(e,$),be=Object.keys,Ne=(e,t)=>{const n=be(e);for(let o=0,r=n.length;o{const n=e.dom;Ne(t,((e,t)=>{((e,t,n)=>{if(!(o(n)||i(n)||d(n)))throw console.error("Invalid call to Attribute.set. Key ",t,":: Value ",n,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,n+"")})(n,t,e)}))},Le=e=>O(e.dom.attributes,((e,t)=>(e[t.name]=t.value,e)),{}),Oe=e=>(e=>$(e.dom.cloneNode(!0)))(e),Ae=(e,t)=>{const n=((e,t)=>{const n=U(t),o=Le(e);return Se(n,o),n})(e,t);var o,r;r=n,(e=>h.from(e.dom.nextSibling).map($))(o=e).fold((()=>{ee(o).each((e=>{ue(e,r)}))}),(e=>{me(e,r)}));const s=te(e);return pe(n,s),he(e),n};var Te=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),xe=tinymce.util.Tools.resolve("tinymce.util.Tools");const ke=e=>t=>l(t)&&t.nodeName.toLowerCase()===e,Ee=e=>t=>l(t)&&e.test(t.nodeName),we=e=>l(e)&&3===e.nodeType,De=e=>l(e)&&1===e.nodeType,Be=Ee(/^(OL|UL|DL)$/),Me=Ee(/^(OL|UL)$/),Pe=ke("ol"),Ie=Ee(/^(LI|DT|DD)$/),Re=Ee(/^(DT|DD)$/),Ue=Ee(/^(TH|TD)$/),$e=ke("br"),_e=(e,t)=>l(t)&&t.nodeName in e.schema.getTextBlockElements(),He=(e,t)=>l(e)&&e.nodeName in t,Fe=(e,t)=>l(t)&&t.nodeName in e.schema.getVoidElements(),Ve=(e,t,n)=>{const o=e.isEmpty(t);return!(n&&e.select("span[data-mce-type=bookmark]",t).length>0)&&o},je=(e,t)=>e.isChildOf(t,e.getRoot()),Ke=e=>t=>t.options.get(e),ze=Ke("lists_indent_on_tab"),Qe=Ke("forced_root_block"),We=Ke("forced_root_block_attrs"),qe=(e,t,n={})=>{const o=e.dom,r=e.schema.getBlockElements(),s=o.createFragment(),i=Qe(e),l=We(e);let a,d,c=!1;for(d=o.create(i,{...l,...n.style?{style:n.style}:{}}),He(t.firstChild,r)||s.appendChild(d);a=t.firstChild;){const e=a.nodeName;c||"SPAN"===e&&"bookmark"===a.getAttribute("data-mce-type")||(c=!0),He(a,r)?(s.appendChild(a),d=null):(d||(d=o.create(i,l),s.appendChild(d)),d.appendChild(a))}return!c&&d&&d.appendChild(o.create("br",{"data-mce-bogus":"1"})),s},Ze=Te.DOM,Ge=Y("dd"),Je=Y("dt"),Xe=(e,t)=>{var n;Ge(t)?Ae(t,"dt"):Je(t)&&(n=t,h.from(n.dom.parentElement).map($)).each((n=>((e,t,n)=>{const o=Ze.select('span[data-mce-type="bookmark"]',t),r=qe(e,n),s=Ze.createRng();s.setStartAfter(n),s.setEndAfter(t);const i=s.extractContents();for(let t=i.firstChild;t;t=t.firstChild)if("LI"===t.nodeName&&e.dom.isEmpty(t)){Ze.remove(t);break}e.dom.isEmpty(i)||Ze.insertAfter(i,t),Ze.insertAfter(r,t);const l=n.parentElement;l&&Ve(e.dom,l)&&(e=>{const t=e.parentNode;t&&xe.each(o,(e=>{t.insertBefore(e,n.parentNode)})),Ze.remove(e)})(l),Ze.remove(n),Ve(e.dom,t)&&Ze.remove(t)})(e,n.dom,t.dom)))},Ye=e=>{Je(e)&&Ae(e,"dd")},et=(e,t)=>{if(we(e))return{container:e,offset:t};const n=fe.getNode(e,t);return we(n)?{container:n,offset:t>=e.childNodes.length?n.data.length:0}:n.previousSibling&&we(n.previousSibling)?{container:n.previousSibling,offset:n.previousSibling.data.length}:n.nextSibling&&we(n.nextSibling)?{container:n.nextSibling,offset:0}:{container:e,offset:t}},tt=e=>{const t=e.cloneRange(),n=et(e.startContainer,e.startOffset);t.setStart(n.container,n.offset);const o=et(e.endContainer,e.endOffset);return t.setEnd(o.container,o.offset),t},nt=["OL","UL","DL"],ot=nt.join(","),rt=(e,t)=>{const n=t||e.selection.getStart(!0);return e.dom.getParent(n,ot,lt(e,n))},st=e=>{const t=e.selection.getSelectedBlocks();return L(((e,t)=>{const n=xe.map(t,(t=>e.dom.getParent(t,"li,dd,dt",lt(e,t))||t));return B(n)})(e,t),Ie)},it=(e,t)=>{const n=e.dom.getParents(t,"TD,TH");return n.length>0?n[0]:e.getBody()},lt=(e,t)=>{const n=e.dom.getParents(t,e.dom.isBlock),o=T(n,(t=>{return(t=>t.nodeName.toLowerCase()!==Qe(e))(t)&&(n=e.schema,!Be(o=t)&&!Ie(o)&&b(nt,(e=>n.isValidChild(o.nodeName,e))));var n,o}));return o.getOr(e.getBody())},at=(e,t)=>{const n=e.dom.getParents(t,"ol,ul",lt(e,t));return D(n)},dt=(e,t)=>{const n=N(t,(t=>at(e,t).getOr(t)));return B(n)},ct=e=>/\btox\-/.test(e.className),mt=(e,t)=>A(e,Be,Ue).exists((e=>e.nodeName===t&&!ct(e))),ut=(e,t)=>null!==t&&!e.dom.isEditable(t),pt=(e,t)=>{const n=e.dom.getParent(t,"ol,ul,dl");return ut(e,n)||!e.selection.isEditable()},gt=(e,t)=>{const n=e.selection.getNode();return t({parents:e.dom.getParents(n),element:n}),e.on("NodeChange",t),()=>e.off("NodeChange",t)},ht=(e,t)=>{const n=(t||document).createDocumentFragment();return S(e,(e=>{n.appendChild(e.dom)})),$(n)},ft=(e,t,n)=>e.dispatch("ListMutation",{action:t,element:n}),yt=(vt=/^\s+|\s+$/g,e=>e.replace(vt,""));var vt;const Ct=(e,t,n)=>{((e,t,n)=>{if(!o(n))throw console.error("Invalid call to CSS.set. Property ",t,":: Value ",n,":: Element ",e),new Error("CSS value must be a string: "+n);(e=>void 0!==e.style&&a(e.style.getPropertyValue))(e)&&e.style.setProperty(t,n)})(e.dom,t,n)},bt=e=>F(e,"OL,UL"),Nt=e=>oe(e).exists(bt),St=e=>"listAttributes"in e,Lt=e=>"isComment"in e,Ot=e=>e.depth>0,At=e=>e.isSelected,Tt=e=>{const t=te(e),n=re(e).exists(bt)?t.slice(0,-1):t;return N(n,Oe)},xt=(e,t)=>{ue(e.item,t.list)},kt=(e,t)=>{const n={list:U(t,e),item:U("li",e)};return ue(n.list,n.item),n},Et=(e,t,n)=>{const o=t.slice(0,n.depth);return D(o).each((t=>{if(St(n)){const o=((e,t,n)=>{const o=U("li",e);return Se(o,t),pe(o,n),o})(e,n.itemAttributes,n.content);((e,t)=>{ue(e.list,t),e.item=t})(t,o),((e,t)=>{Q(e.list)!==t.listType&&(e.list=Ae(e.list,t.listType)),Se(e.list,t.listAttributes)})(t,n)}else if((e=>"isFragment"in e)(n))pe(t.item,n.content);else{const e=R(`\x3c!--${n.content}--\x3e`);ue(t.list,e)}})),o},wt=(e,t)=>{let n=h.none();const o=O(t,((t,o,r)=>Lt(o)?0===r?(n=h.some(o),t):Et(e,t,o):o.depth>t.length?((e,t,n)=>{const o=((e,t,n)=>{const o=[];for(let r=0;r{for(let t=1;t{for(let t=0;t{St(t)&&(Se(e.list,t.listAttributes),Se(e.item,t.itemAttributes)),pe(e.item,t.content)}))})(o,n),r=o,P(D(t),w(r),xt),t.concat(o)})(e,t,o):Et(e,t,o)),[]);return n.each((e=>{const t=R(`\x3c!--${e.content}--\x3e`);w(o).each((e=>{((e,t)=>{oe(e).fold((()=>{ue(e,t)}),(n=>{e.dom.insertBefore(t.dom,n.dom)}))})(e.list,t)}))})),w(o).map((e=>e.list))},Dt=e=>(S(e,((t,n)=>{((e,t)=>{const n=e[t].depth,o=e=>e.depth===n&&!e.dirty,r=e=>e.depthA(e.slice(t+1),o,r)))})(e,n).fold((()=>{t.dirty&&St(t)&&(e=>{e.listAttributes=((e,t)=>{const n={};var o;return((e,t,n,o)=>{Ne(e,((e,r)=>{(t(e,r)?n:o)(e,r)}))})(e,t,(o=n,(e,t)=>{o[t]=e}),c),n})(e.listAttributes,((e,t)=>"start"!==t))})(t)}),(e=>{return o=e,void(St(n=t)&&St(o)&&(n.listType=o.listType,n.listAttributes={...o.listAttributes}));var n,o}))})),e),Bt=(e,t,n,o)=>{var r,s;if(8===W(s=o)||"#comment"===Q(s))return[{depth:e+1,content:null!==(r=o.dom.nodeValue)&&void 0!==r?r:"",dirty:!1,isSelected:!1,isComment:!0}];t.each((e=>{H(e.start,o)&&n.set(!0)}));const i=((e,t,n)=>ee(e).filter(G).map((o=>({depth:t,dirty:!1,isSelected:n,content:Tt(e),itemAttributes:Le(e),listAttributes:Le(o),listType:Q(o),isInPreviousLi:!1}))))(o,e,n.get());t.each((e=>{H(e.end,o)&&n.set(!1)}));const l=re(o).filter(bt).map((o=>Pt(e,t,n,o))).getOr([]);return i.toArray().concat(l)},Mt=(e,t,n,o)=>oe(o).filter(bt).fold((()=>Bt(e,t,n,o)),(r=>{const s=O(te(o),((o,s,i)=>{if(0===i)return o;if(F(s,"LI"))return o.concat(Bt(e,t,n,s));{const t={isFragment:!0,depth:e,content:[s],isSelected:!1,dirty:!1,parentListType:Q(r)};return o.concat(t)}}),[]);return Pt(e,t,n,r).concat(s)})),Pt=(e,t,n,o)=>x(te(o),(o=>(bt(o)?Pt:Mt)(e+1,t,n,o))),It=(e,t,n)=>{const o=((e,t)=>{const n=(()=>{let e=!1;return{get:()=>e,set:t=>{e=t}}})();return N(e,(e=>({sourceList:e,entries:Pt(0,t,n,e)})))})(t,(e=>{const t=N(st(e),$);return P(T(t,p(Nt)),T(k(t),p(Nt)),((e,t)=>({start:e,end:t})))})(e));S(o,(t=>{((e,t)=>{S(L(e,At),(e=>((e,t)=>{switch(e){case"Indent":t.depth++;break;case"Outdent":t.depth--;break;case"Flatten":t.depth=0}t.dirty=!0})(t,e)))})(t.entries,n);const o=((e,t)=>x(((e,t)=>{if(0===e.length)return[];{let n=t(e[0]);const o=[];let r=[];for(let s=0,i=e.length;sw(t).exists(Ot)?((e,t)=>{const n=Dt(t);return wt(e.contentDocument,n).toArray()})(e,t):((e,t)=>{const n=Dt(t);return N(n,(t=>{const n=Lt(t)?ht([R(`\x3c!--${t.content}--\x3e`)]):ht(t.content),o=St(t)?t.itemAttributes:{};return $(qe(e,n.dom,o))}))})(e,t))))(e,t.entries);var r;S(o,(t=>{ft(e,"Indent"===n?"IndentList":"OutdentList",t.dom)})),r=t.sourceList,S(o,(e=>{me(r,e)})),he(t.sourceList)}))},Rt=(e,t)=>{const n=Ce((e=>{const t=(e=>{const t=at(e,e.selection.getStart()),n=L(e.selection.getSelectedBlocks(),Me);return t.toArray().concat(n)})(e),n=(e=>{const t=e.selection.getStart();return e.dom.getParents(t,"ol,ul",lt(e,t))})(e);return T(n,(e=>{return t=$(e),ee(t).exists((e=>Ie(e.dom)&&oe(e).exists((e=>!Be(e.dom)))&&re(e).exists((e=>!Be(e.dom)))));var t})).fold((()=>dt(e,t)),(e=>[e]))})(e)),o=Ce((e=>L(st(e),Re))(e));let r=!1;if(n.length||o.length){const s=e.selection.getBookmark();It(e,n,t),((e,t,n)=>{S(n,"Indent"===t?Ye:t=>Xe(e,t))})(e,t,o),e.selection.moveToBookmark(s),e.selection.setRng(tt(e.selection.getRng())),e.nodeChanged(),r=!0}return r},Ut=(e,t)=>!(e=>{const t=rt(e);return ut(e,t)||!e.selection.isEditable()})(e)&&Rt(e,t),$t=e=>Ut(e,"Indent"),_t=e=>Ut(e,"Outdent"),Ht=e=>Ut(e,"Flatten"),Ft=e=>"\ufeff"===e;var Vt=tinymce.util.Tools.resolve("tinymce.dom.BookmarkManager");const jt=Te.DOM,Kt=e=>{const t={},n=n=>{let o=e[n?"startContainer":"endContainer"],r=e[n?"startOffset":"endOffset"];if(De(o)){const e=jt.create("span",{"data-mce-type":"bookmark"});o.hasChildNodes()?(r=Math.min(r,o.childNodes.length-1),n?o.insertBefore(e,o.childNodes[r]):jt.insertAfter(e,o.childNodes[r])):o.appendChild(e),o=e,r=0}t[n?"startContainer":"endContainer"]=o,t[n?"startOffset":"endOffset"]=r};return n(!0),e.collapsed||n(),t},zt=e=>{const t=t=>{let n=e[t?"startContainer":"endContainer"],o=e[t?"startOffset":"endOffset"];if(n){if(De(n)&&n.parentNode){const e=n;o=(e=>{var t;let n=null===(t=e.parentNode)||void 0===t?void 0:t.firstChild,o=0;for(;n;){if(n===e)return o;De(n)&&"bookmark"===n.getAttribute("data-mce-type")||o++,n=n.nextSibling}return-1})(n),n=n.parentNode,jt.remove(e),!n.hasChildNodes()&&jt.isBlock(n)&&n.appendChild(jt.create("br"))}e[t?"startContainer":"endContainer"]=n,e[t?"startOffset":"endOffset"]=o}};t(!0),t();const n=jt.createRng();return n.setStart(e.startContainer,e.startOffset),e.endContainer&&n.setEnd(e.endContainer,e.endOffset),tt(n)},Qt=e=>{switch(e){case"UL":return"ToggleUlList";case"OL":return"ToggleOlList";case"DL":return"ToggleDLList"}},Wt=(e,t)=>{xe.each(t,((t,n)=>{e.setAttribute(n,t)}))},qt=(e,t,n)=>{((e,t,n)=>{const o=n["list-style-type"]?n["list-style-type"]:null;e.setStyle(t,"list-style-type",o)})(e,t,n),((e,t,n)=>{Wt(t,n["list-attributes"]),xe.each(e.select("li",t),(e=>{Wt(e,n["list-item-attributes"])}))})(e,t,n)},Zt=(e,t)=>l(t)&&!He(t,e.schema.getBlockElements()),Gt=(e,t,n,o)=>{let r=t[n?"startContainer":"endContainer"];const s=t[n?"startOffset":"endOffset"];De(r)&&(r=r.childNodes[Math.min(s,r.childNodes.length-1)]||r),!n&&$e(r.nextSibling)&&(r=r.nextSibling);const i=(t,n)=>{var r;const s=new ye(t,(t=>{for(;!e.dom.isBlock(t)&&t.parentNode&&o!==t;)t=t.parentNode;return t})(t)),i=n?"next":"prev";let l;for(;l=s[i]();)if(!Fe(e,l)&&!Ft(l.textContent)&&0!==(null===(r=l.textContent)||void 0===r?void 0:r.length))return h.some(l);return h.none()};if(n&&we(r))if(Ft(r.textContent))r=i(r,!1).getOr(r);else for(null!==r.parentNode&&Zt(e,r.parentNode)&&(r=r.parentNode);null!==r.previousSibling&&(Zt(e,r.previousSibling)||we(r.previousSibling));)r=r.previousSibling;if(!n&&we(r))if(Ft(r.textContent))r=i(r,!0).getOr(r);else for(null!==r.parentNode&&Zt(e,r.parentNode)&&(r=r.parentNode);null!==r.nextSibling&&(Zt(e,r.nextSibling)||we(r.nextSibling));)r=r.nextSibling;for(;r.parentNode!==o;){const t=r.parentNode;if(_e(e,r))return r;if(/^(TD|TH)$/.test(t.nodeName))return r;r=t}return r},Jt=(e,t,n)=>{const o=e.selection.getRng();let r="LI";const s=lt(e,((e,t)=>{const n=e.selection.getStart(!0),o=Gt(e,t,!0,e.getBody());return r=$(o),s=$(t.commonAncestorContainer),i=r,l=function(e,...t){return(...n)=>{const o=t.concat(n);return e.apply(null,o)}}(H,s),ae(i,l,void 0).isSome()?t.commonAncestorContainer:n;var r,s,i,l})(e,o)),i=e.dom;if("false"===i.getContentEditable(e.selection.getNode()))return;"DL"===(t=t.toUpperCase())&&(r="DT");const l=Kt(o),a=L(((e,t,n)=>{const o=[],r=e.dom,s=Gt(e,t,!0,n),i=Gt(e,t,!1,n);let l;const a=[];for(let e=s;e&&(a.push(e),e!==i);e=e.nextSibling);return xe.each(a,(t=>{var s;if(_e(e,t))return o.push(t),void(l=null);if(r.isBlock(t)||$e(t))return $e(t)&&r.remove(t),void(l=null);const i=t.nextSibling;Vt.isBookmarkNode(t)&&(Be(i)||_e(e,i)||!i&&t.parentNode===n)?l=null:(l||(l=r.create("p"),null===(s=t.parentNode)||void 0===s||s.insertBefore(l,t),o.push(l)),l.appendChild(t))})),o})(e,o,s),e.dom.isEditable);xe.each(a,(o=>{let s;const l=o.previousSibling,a=o.parentNode;Ie(a)||(l&&Be(l)&&l.nodeName===t&&((e,t,n)=>{const o=e.getStyle(t,"list-style-type");let r=n?n["list-style-type"]:"";return r=null===r?"":r,o===r})(i,l,n)?(s=l,o=i.rename(o,r),l.appendChild(o)):(s=i.create(t),a.insertBefore(s,o),s.appendChild(o),o=i.rename(o,r)),((e,t)=>{xe.each(["margin","margin-right","margin-bottom","margin-left","margin-top","padding","padding-right","padding-bottom","padding-left","padding-top"],(n=>e.setStyle(t,n,"")))})(i,o),qt(i,s,n),Yt(e.dom,s))})),e.selection.setRng(zt(l))},Xt=(e,t,n)=>{return((e,t)=>Be(e)&&e.nodeName===(null==t?void 0:t.nodeName))(t,n)&&((e,t,n)=>e.getStyle(t,"list-style-type",!0)===e.getStyle(n,"list-style-type",!0))(e,t,n)&&(o=n,t.className===o.className);var o},Yt=(e,t)=>{let n,o=t.nextSibling;if(Xt(e,t,o)){const r=o;for(;n=r.firstChild;)t.appendChild(n);e.remove(r)}if(o=t.previousSibling,Xt(e,t,o)){const r=o;for(;n=r.lastChild;)t.insertBefore(n,t.firstChild);e.remove(r)}},en=(e,t,n,o)=>{if(t.nodeName!==n){const r=e.dom.rename(t,n);qt(e.dom,r,o),ft(e,Qt(n),r)}else qt(e.dom,t,o),ft(e,Qt(n),t)},tn=(e,t,n,o)=>{if(t.classList.forEach(((e,n,o)=>{e.startsWith("tox-")&&(o.remove(e),0===o.length&&t.removeAttribute("class"))})),t.nodeName!==n){const r=e.dom.rename(t,n);qt(e.dom,r,o),ft(e,Qt(n),r)}else qt(e.dom,t,o),ft(e,Qt(n),t)},nn=e=>"list-style-type"in e,on=(e,t,n)=>{const o=rt(e);if(pt(e,o))return;const s=(e=>{const t=rt(e),n=e.selection.getSelectedBlocks();return((e,t)=>l(e)&&1===t.length&&t[0]===e)(t,n)?(e=>L(e.querySelectorAll(ot),Be))(t):L(n,(e=>Be(e)&&t!==e))})(e),i=r(n)?n:{};s.length>0?((e,t,n,o,r)=>{const s=Be(t);if(!s||t.nodeName!==o||nn(r)||ct(t)){Jt(e,o,r);const i=Kt(e.selection.getRng()),l=s?[t,...n]:n,a=s&&ct(t)?tn:en;xe.each(l,(t=>{a(e,t,o,r)})),e.selection.setRng(zt(i))}else Ht(e)})(e,o,s,t,i):((e,t,n,o)=>{if(t!==e.getBody())if(t)if(t.nodeName!==n||nn(o)||ct(t)){const r=Kt(e.selection.getRng());ct(t)&&t.classList.forEach(((e,n,o)=>{e.startsWith("tox-")&&(o.remove(e),0===o.length&&t.removeAttribute("class"))})),qt(e.dom,t,o);const s=e.dom.rename(t,n);Yt(e.dom,s),e.selection.setRng(zt(r)),Jt(e,n,o),ft(e,Qt(n),s)}else Ht(e);else Jt(e,n,o),ft(e,Qt(n),t)})(e,o,t,i)},rn=Te.DOM,sn=(e,t)=>{const n=xe.grep(e.select("ol,ul",t));xe.each(n,(t=>{((e,t)=>{const n=t.parentElement;if(n&&"LI"===n.nodeName&&n.firstChild===t){const o=n.previousSibling;o&&"LI"===o.nodeName?(o.appendChild(t),Ve(e,n)&&rn.remove(n)):rn.setStyle(n,"listStyleType","none")}if(Be(n)){const e=n.previousSibling;e&&"LI"===e.nodeName&&e.appendChild(t)}})(e,t)}))},ln=(e,t,n,o)=>{let r=t.startContainer;const s=t.startOffset;if(we(r)&&(n?s0))return r;const i=e.schema.getNonEmptyElements();De(r)&&(r=fe.getNode(r,s));const l=new ye(r,o);n&&((e,t)=>!!$e(t)&&e.isBlock(t.nextSibling)&&!$e(t.previousSibling))(e.dom,r)&&l.next();const a=n?l.next.bind(l):l.prev2.bind(l);for(;r=a();){if("LI"===r.nodeName&&!r.hasChildNodes())return r;if(i[r.nodeName])return r;if(we(r)&&r.data.length>0)return r}return null},an=(e,t)=>{const n=t.childNodes;return 1===n.length&&!Be(n[0])&&e.isBlock(n[0])},dn=e=>h.from(e).map($).filter(Z).exists((e=>((e,t=!1)=>{return ie(e)?e.dom.isContentEditable:(n=e,le(((e,t)=>_(e,t)),ce,n,"[contenteditable]",void 0)).fold(m(t),(e=>"true"===(e=>e.dom.contentEditable)(e)));var n})(e)&&!C(["details"],Q(e)))),cn=(e,t,n)=>{let o;const r=an(e,n)?n.firstChild:n;if(((e,t)=>{an(e,t)&&dn(t.firstChild)&&e.remove(t.firstChild,!0)})(e,t),!Ve(e,t,!0))for(;o=t.firstChild;)r.appendChild(o)},mn=(e,t,n)=>{let o;const r=t.parentNode;if(!je(e,t)||!je(e,n))return;Be(n.lastChild)&&(o=n.lastChild),r===n.lastChild&&$e(r.previousSibling)&&e.remove(r.previousSibling);const s=n.lastChild;s&&$e(s)&&t.hasChildNodes()&&e.remove(s),Ve(e,n,!0)&&ge($(n)),cn(e,t,n),o&&n.appendChild(o);const i=((e,t)=>{const n=e.dom,o=t.dom;return n!==o&&n.contains(o)})($(n),$(t))?e.getParents(t,Be,n):[];e.remove(t),S(i,(t=>{Ve(e,t)&&t!==e.getRoot()&&e.remove(t)}))},un=(e,t)=>{const n=e.dom,o=e.selection,r=o.getStart(),s=it(e,r),i=n.getParent(o.getStart(),"LI",s);if(i){const r=i.parentElement;if(r===e.getBody()&&Ve(n,r))return!0;const l=tt(o.getRng()),a=n.getParent(ln(e,l,t,s),"LI",s),d=a&&(t?n.isChildOf(i,a):n.isChildOf(a,i));if(a&&a!==i&&!d)return e.undoManager.transact((()=>{var n,o;t?((e,t,n,o)=>{const r=e.dom;if(r.isEmpty(o))((e,t,n)=>{ge($(n)),mn(e.dom,t,n),e.selection.setCursorLocation(n,0)})(e,n,o);else{const s=Kt(t);mn(r,n,o),e.selection.setRng(zt(s))}})(e,l,a,i):(null===(o=(n=i).parentNode)||void 0===o?void 0:o.firstChild)===n?_t(e):((e,t,n,o)=>{const r=Kt(t);mn(e.dom,n,o);const s=zt(r);e.selection.setRng(s)})(e,l,i,a)})),!0;if(d&&!t&&a!==i){const t=l.commonAncestorContainer.parentElement;return!(!t||n.isChildOf(a,t)||(e.undoManager.transact((()=>{const o=Kt(l);cn(n,t,a),t.remove();const r=zt(o);e.selection.setRng(r)})),0))}if(!a&&!t&&0===l.startOffset&&0===l.endOffset)return e.undoManager.transact((()=>{Ht(e)})),!0}return!1},pn=e=>{const t=e.selection.getStart(),n=it(e,t);return e.dom.getParent(t,"LI,DT,DD",n)||st(e).length>0},gn=(e,t)=>{const n=e.selection;return!pt(e,n.getNode())&&(n.isCollapsed()?((e,t)=>un(e,t)||((e,t)=>{const n=e.dom,o=e.selection.getStart(),r=it(e,o),s=n.getParent(o,n.isBlock,r);if(s&&n.isEmpty(s,void 0,{checkRootAsContent:!0})){const o=tt(e.selection.getRng()),i=ln(e,o,t,r),l=n.getParent(i,"LI",r);if(i&&l){const a=e=>C(["td","th","caption"],Q(e)),d=e=>e.dom===r;return!!((e,t,n=u)=>P(e,t,n).getOr(e.isNone()&&t.isNone()))(de($(l),a,d),de($(o.startContainer),a,d),H)&&(e.undoManager.transact((()=>{const o=l.parentNode;((e,t,n)=>{const o=e.getParent(t.parentNode,e.isBlock,n);e.remove(t),o&&e.isEmpty(o)&&e.remove(o)})(n,s,r),Yt(n,o),e.selection.select(i,!0),e.selection.collapse(t)})),!0)}}return!1})(e,t))(e,t):(e=>!!pn(e)&&(e.undoManager.transact((()=>{let t=!0;const n=()=>t=!1;e.on("input",n),e.execCommand("Delete"),e.off("input",n),t&&e.dispatch("input"),sn(e.dom,e.getBody())})),!0))(e))},hn=e=>{const t=k(yt(e).split("")),n=N(t,((e,t)=>{const n=e.toUpperCase().charCodeAt(0)-"A".charCodeAt(0)+1;return Math.pow(26,t)*n}));return O(n,((e,t)=>e+t),0)},fn=e=>{if(--e<0)return"";{const t=e%26,n=Math.floor(e/26);return fn(n)+String.fromCharCode("A".charCodeAt(0)+t)}},yn=e=>{const t=parseInt(e.start,10);return M(e.listStyleType,"upper-alpha")?fn(t):M(e.listStyleType,"lower-alpha")?fn(t).toLowerCase():e.start},vn=(e,t)=>()=>{const n=rt(e);return l(n)&&n.nodeName===t},Cn=e=>{e.addCommand("mceListProps",(()=>{(e=>{const t=rt(e);Pe(t)&&!pt(e,t)&&e.windowManager.open({title:"List Properties",body:{type:"panel",items:[{type:"input",name:"start",label:"Start list at number",inputMode:"numeric"}]},initialData:{start:yn({start:e.dom.getAttrib(t,"start","1"),listStyleType:h.from(e.dom.getStyle(t,"list-style-type"))})},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],onSubmit:t=>{(e=>{switch((e=>/^[0-9]+$/.test(e)?2:/^[A-Z]+$/.test(e)?0:/^[a-z]+$/.test(e)?1:e.length>0?4:3)(e)){case 2:return h.some({listStyleType:h.none(),start:e});case 0:return h.some({listStyleType:h.some("upper-alpha"),start:hn(e).toString()});case 1:return h.some({listStyleType:h.some("lower-alpha"),start:hn(e).toString()});case 3:return h.some({listStyleType:h.none(),start:""});case 4:return h.none()}})(t.getData().start).each((t=>{e.execCommand("mceListUpdate",!1,{attrs:{start:"1"===t.start?"":t.start},styles:{"list-style-type":t.listStyleType.getOr("")}})})),t.close()}})})(e)}))};var bn=tinymce.util.Tools.resolve("tinymce.html.Node");const Nn=e=>3===e.type,Sn=e=>0===e.length,Ln=e=>{const t=(t,n)=>{const o=bn.create("li");S(t,(e=>o.append(e))),n?e.insert(o,n,!0):e.append(o)},n=O(e.children(),((e,n)=>Nn(n)?[...e,n]:Sn(e)||Nn(n)?e:(t(e,n),[])),[]);Sn(n)||t(n)},On=(e,t)=>n=>(n.setEnabled(e.selection.isEditable()),gt(e,(o=>{n.setActive(mt(o.parents,t)),n.setEnabled(!pt(e,o.element)&&e.selection.isEditable())}))),An=(e,t)=>n=>gt(e,(o=>n.setEnabled(mt(o.parents,t)&&!pt(e,o.element))));e.add("lists",(e=>((e=>{(0,e.options.register)("lists_indent_on_tab",{processor:"boolean",default:!0})})(e),(e=>{e.on("PreInit",(()=>{const{parser:t}=e;t.addNodeFilter("ul,ol",(e=>S(e,Ln)))}))})(e),e.hasPlugin("rtc",!0)?Cn(e):((e=>{ze(e)&&(e=>{e.on("keydown",(t=>{t.keyCode!==ve.TAB||ve.metaKeyPressed(t)||e.undoManager.transact((()=>{(t.shiftKey?_t(e):$t(e))&&t.preventDefault()}))}))})(e),(e=>{e.on("ExecCommand",(t=>{const n=t.command.toLowerCase();"delete"!==n&&"forwarddelete"!==n||!pn(e)||sn(e.dom,e.getBody())})),e.on("keydown",(t=>{t.keyCode===ve.BACKSPACE?gn(e,!1)&&t.preventDefault():t.keyCode===ve.DELETE&&gn(e,!0)&&t.preventDefault()}))})(e)})(e),(e=>{e.on("BeforeExecCommand",(t=>{const n=t.command.toLowerCase();"indent"===n?$t(e):"outdent"===n&&_t(e)})),e.addCommand("InsertUnorderedList",((t,n)=>{on(e,"UL",n)})),e.addCommand("InsertOrderedList",((t,n)=>{on(e,"OL",n)})),e.addCommand("InsertDefinitionList",((t,n)=>{on(e,"DL",n)})),e.addCommand("RemoveList",(()=>{Ht(e)})),Cn(e),e.addCommand("mceListUpdate",((t,n)=>{r(n)&&((e,t)=>{const n=rt(e);null===n||pt(e,n)||e.undoManager.transact((()=>{r(t.styles)&&e.dom.setStyles(n,t.styles),r(t.attrs)&&Ne(t.attrs,((t,o)=>e.dom.setAttrib(n,o,t)))}))})(e,n)})),e.addQueryStateHandler("InsertUnorderedList",vn(e,"UL")),e.addQueryStateHandler("InsertOrderedList",vn(e,"OL")),e.addQueryStateHandler("InsertDefinitionList",vn(e,"DL"))})(e)),(e=>{const t=t=>()=>e.execCommand(t);e.hasPlugin("advlist")||(e.ui.registry.addToggleButton("numlist",{icon:"ordered-list",active:!1,tooltip:"Numbered list",onAction:t("InsertOrderedList"),onSetup:On(e,"OL")}),e.ui.registry.addToggleButton("bullist",{icon:"unordered-list",active:!1,tooltip:"Bullet list",onAction:t("InsertUnorderedList"),onSetup:On(e,"UL")}))})(e),(e=>{const t={text:"List properties...",icon:"ordered-list",onAction:()=>e.execCommand("mceListProps"),onSetup:An(e,"OL")};e.ui.registry.addMenuItem("listprops",t),e.ui.registry.addContextMenu("lists",{update:t=>{const n=rt(e,t);return Pe(n)?["listprops"]:[]}})})(e),(e=>({backspaceDelete:t=>{gn(e,t)}}))(e))))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/media/plugin.min.js b/src/main/webapp/content/tinymce/plugins/media/plugin.min.js new file mode 100644 index 0000000..5a31076 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/media/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(r=o=e,(a=String).prototype.isPrototypeOf(r)||(null===(s=o.constructor)||void 0===s?void 0:s.name)===a.name)?"string":t;var r,o,a,s})(t)===e,r=t("string"),o=t("object"),a=t("array"),s=e=>!(e=>null==e)(e);class i{constructor(e,t){this.tag=e,this.value=t}static some(e){return new i(!0,e)}static none(){return i.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?i.some(e(this.value)):i.none()}bind(e){return this.tag?e(this.value):i.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:i.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return s(e)?i.some(e):i.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}i.singletonNone=new i(!1);const n=Array.prototype.push,l=(e,t)=>{for(let r=0,o=e.length;r{const t=[];for(let r=0,o=e.length;rh(e,t)?i.from(e[t]):i.none(),h=(e,t)=>u.call(e,t),p=e=>t=>t.options.get(e),g=p("audio_template_callback"),b=p("video_template_callback"),w=p("iframe_template_callback"),v=p("media_live_embeds"),f=p("media_filter_html"),y=p("media_url_resolver"),x=p("media_alt_source"),_=p("media_poster"),k=p("media_dimensions");var j=tinymce.util.Tools.resolve("tinymce.util.Tools"),O=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),A=tinymce.util.Tools.resolve("tinymce.html.DomParser");const S=O.DOM,$=e=>e.replace(/px$/,""),C=e=>{const t=e.attr("style"),r=t?S.parseStyle(t):{};return{type:"ephox-embed-iri",source:e.attr("data-ephox-embed-iri"),altsource:"",poster:"",width:d(r,"max-width").map($).getOr(""),height:d(r,"max-height").map($).getOr("")}},T=(e,t)=>{let r={};for(let o=A({validate:!1,forced_root_block:!1},t).parse(e);o;o=o.walk())if(1===o.type){const e=o.name;if(o.attr("data-ephox-embed-iri")){r=C(o);break}r.source||"param"!==e||(r.source=o.attr("movie")),"iframe"!==e&&"object"!==e&&"embed"!==e&&"video"!==e&&"audio"!==e||(r.type||(r.type=e),r=j.extend(o.attributes.map,r)),"source"===e&&(r.source?r.altsource||(r.altsource=o.attr("src")):r.source=o.attr("src")),"img"!==e||r.poster||(r.poster=o.attr("src"))}return r.source=r.source||r.src||"",r.altsource=r.altsource||"",r.poster=r.poster||"",r},z=e=>{var t;const r=null!==(t=e.toLowerCase().split(".").pop())&&void 0!==t?t:"";return d({mp3:"audio/mpeg",m4a:"audio/x-m4a",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",swf:"application/x-shockwave-flash"},r).getOr("")};var D=tinymce.util.Tools.resolve("tinymce.html.Node"),F=tinymce.util.Tools.resolve("tinymce.html.Serializer");const M=(e,t={})=>A({forced_root_block:!1,validate:!1,allow_conditional_comments:!0,...t},e),N=O.DOM,P=e=>/^[0-9.]+$/.test(e)?e+"px":e,R=(e,t)=>{const r=t.attr("style"),o=r?N.parseStyle(r):{};s(e.width)&&(o["max-width"]=P(e.width)),s(e.height)&&(o["max-height"]=P(e.height)),t.attr("style",N.serializeStyle(o))},E=["source","altsource"],U=(e,t,r,o)=>{let a=0,s=0;const i=M(o);i.addNodeFilter("source",(e=>a=e.length));const n=i.parse(e);for(let e=n;e;e=e.walk())if(1===e.type){const o=e.name;if(e.attr("data-ephox-embed-iri")){R(t,e);break}switch(o){case"video":case"object":case"embed":case"img":case"iframe":void 0!==t.height&&void 0!==t.width&&(e.attr("width",t.width),e.attr("height",t.height))}if(r)switch(o){case"video":e.attr("poster",t.poster),e.attr("src",null);for(let r=a;r<2;r++)if(t[E[r]]){const o=new D("source",1);o.attr("src",t[E[r]]),o.attr("type",t[E[r]+"mime"]||null),e.append(o)}break;case"iframe":e.attr("src",t.source);break;case"object":const r=e.getAll("img").length>0;if(t.poster&&!r){e.attr("src",t.poster);const r=new D("img",1);r.attr("src",t.poster),r.attr("width",t.width),r.attr("height",t.height),e.append(r)}break;case"source":if(s<2&&(e.attr("src",t[E[s]]),e.attr("type",t[E[s]+"mime"]||null),!t[E[s]])){e.remove();continue}s++;break;case"img":t.poster||e.remove()}}return F({},o).serialize(n)},L=[{regex:/youtu\.be\/([\w\-_\?&=.]+)/i,type:"iframe",w:560,h:314,url:"www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?/i,type:"iframe",w:560,h:314,url:"www.youtube.com/embed/$2?$4",allowFullscreen:!0},{regex:/youtube.com\/embed\/([a-z0-9\?&=\-_]+)/i,type:"iframe",w:560,h:314,url:"www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/vimeo\.com\/([0-9]+)\?h=(\w+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$1?h=$2&title=0&byline=0&portrait=0&color=8dc7dc",allowFullscreen:!0},{regex:/vimeo\.com\/(.*)\/([0-9]+)\?h=(\w+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$2?h=$3&title=0&byline=0",allowFullscreen:!0},{regex:/vimeo\.com\/([0-9]+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$1?title=0&byline=0&portrait=0&color=8dc7dc",allowFullscreen:!0},{regex:/vimeo\.com\/(.*)\/([0-9]+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$2?title=0&byline=0",allowFullscreen:!0},{regex:/maps\.google\.([a-z]{2,3})\/maps\/(.+)msid=(.+)/,type:"iframe",w:425,h:350,url:'maps.google.com/maps/ms?msid=$2&output=embed"',allowFullscreen:!1},{regex:/dailymotion\.com\/video\/([^_]+)/,type:"iframe",w:480,h:270,url:"www.dailymotion.com/embed/video/$1",allowFullscreen:!0},{regex:/dai\.ly\/([^_]+)/,type:"iframe",w:480,h:270,url:"www.dailymotion.com/embed/video/$1",allowFullscreen:!0}],I=(e,t)=>{const r=(e=>{const t=e.match(/^(https?:\/\/|www\.)(.+)$/i);return t&&t.length>1?"www."===t[1]?"https://":t[1]:"https://"})(t),o=e.regex.exec(t);let a=r+e.url;if(s(o))for(let e=0;eo[e]?o[e]:""));return a.replace(/\?$/,"")},B=e=>{const t=L.filter((t=>t.regex.test(e)));return t.length>0?j.extend({},t[0],{url:I(t[0],e)}):null},G=(e,t)=>{var r;const o=j.extend({},t);if(!o.source&&(j.extend(o,T(null!==(r=o.embed)&&void 0!==r?r:"",e.schema)),!o.source))return"";o.altsource||(o.altsource=""),o.poster||(o.poster=""),o.source=e.convertURL(o.source,"source"),o.altsource=e.convertURL(o.altsource,"source"),o.sourcemime=z(o.source),o.altsourcemime=z(o.altsource),o.poster=e.convertURL(o.poster,"poster");const a=B(o.source);if(a&&(o.source=a.url,o.type=a.type,o.allowfullscreen=a.allowFullscreen,o.width=o.width||String(a.w),o.height=o.height||String(a.h)),o.embed)return U(o.embed,o,!0,e.schema);{const t=g(e),r=b(e),a=w(e);return o.width=o.width||"300",o.height=o.height||"150",j.each(o,((t,r)=>{o[r]=e.dom.encode(""+t)})),"iframe"===o.type?((e,t)=>{if(t)return t(e);{const t=e.allowfullscreen?' allowFullscreen="1"':"";return'"}})(o,a):"application/x-shockwave-flash"===o.sourcemime?(e=>{let t='';return e.poster&&(t+=''),t+="",t})(o):-1!==o.sourcemime.indexOf("audio")?((e,t)=>t?t(e):'")(o,t):((e,t)=>t?t(e):'")(o,r)}},W=e=>e.hasAttribute("data-mce-object")||e.hasAttribute("data-ephox-embed-iri"),q={},H=e=>t=>G(e,t),J=(e,t)=>{const r=y(e);return r?((e,t,r)=>new Promise(((o,a)=>{const s=r=>(r.html&&(q[e.source]=r),o({url:e.source,html:r.html?r.html:t(e)}));q[e.source]?s(q[e.source]):r({url:e.source}).then(s).catch(a)})))(t,H(e),r):((e,t)=>Promise.resolve({html:t(e),url:e.source}))(t,H(e))},K=(e,t)=>{const r={};return d(e,"dimensions").each((e=>{l(["width","height"],(o=>{d(t,o).orThunk((()=>d(e,o))).each((e=>r[o]=e))}))})),r},Q=(e,t)=>{const r=t&&"dimensions"!==t?((e,t)=>d(t,e).bind((e=>d(e,"meta"))))(t,e).getOr({}):{},a=((e,t,r)=>a=>{const s=()=>d(e,a),n=()=>d(t,a),l=e=>d(e,"value").bind((e=>e.length>0?i.some(e):i.none()));return{[a]:(a===r?s().bind((e=>o(e)?l(e).orThunk(n):n().orThunk((()=>i.from(e))))):n().orThunk((()=>s().bind((e=>o(e)?l(e):i.from(e)))))).getOr("")}})(e,r,t);return{...a("source"),...a("altsource"),...a("poster"),...a("embed"),...K(e,r)}},V=e=>{const t={...e,source:{value:d(e,"source").getOr("")},altsource:{value:d(e,"altsource").getOr("")},poster:{value:d(e,"poster").getOr("")}};return l(["width","height"],(r=>{d(e,r).each((e=>{const o=t.dimensions||{};o[r]=e,t.dimensions=o}))})),t},X=e=>t=>{const r=t&&t.msg?"Media embed handler error: "+t.msg:"Media embed handler threw unknown error.";e.notificationManager.open({type:"error",text:r})},Y=(e,t)=>o=>{if(r(o.url)&&o.url.trim().length>0){const r=o.html,a={...T(r,t.schema),source:o.url,embed:r};e.setData(V(a))}},Z=(e,t)=>{const r=e.dom.select("*[data-mce-object]");e.insertContent(t),((e,t)=>{const r=e.dom.select("*[data-mce-object]");for(let e=0;e=0;o--)t[e]===r[o]&&r.splice(o,1);e.selection.select(r[0])})(e,r),e.nodeChanged()},ee=(e,t)=>s(t)&&"ephox-embed-iri"===t&&s(B(e)),te=(e,t)=>((e,t)=>e.width!==t.width||e.height!==t.height)(e,t)&&ee(t.source,e.type),re=e=>{const t=(e=>{const t=e.selection.getNode(),r=W(t)?e.serializer.serialize(t,{selection:!0}):"",o=T(r,e.schema),a=(()=>{if(ee(o.source,o.type)){const r=e.dom.getRect(t);return{width:r.w.toString().replace(/px$/,""),height:r.h.toString().replace(/px$/,"")}}return{}})();return{embed:r,...o,...a}})(e),r=(e=>{let t=e;return{get:()=>t,set:e=>{t=e}}})(t),o=V(t),a=k(e)?[{type:"sizeinput",name:"dimensions",label:"Constrain proportions",constrain:!0}]:[],s={title:"General",name:"general",items:c([[{name:"source",type:"urlinput",filetype:"media",label:"Source",picker_text:"Browse files"}],a])},i=[];x(e)&&i.push({name:"altsource",type:"urlinput",filetype:"media",label:"Alternative source URL"}),_(e)&&i.push({name:"poster",type:"urlinput",filetype:"image",label:"Media poster (Image URL)"});const n={title:"Advanced",name:"advanced",items:i},l=[s,{title:"Embed",items:[{type:"textarea",name:"embed",label:"Paste your embed code below:"}]}];i.length>0&&l.push(n);const m={type:"tabpanel",tabs:l},u=e.windowManager.open({title:"Insert/Edit Media",size:"normal",body:m,buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],onSubmit:t=>{const o=Q(t.getData());((e,t,r)=>{var o,a;t.embed=te(e,t)&&k(r)?G(r,{...t,embed:""}):U(null!==(o=t.embed)&&void 0!==o?o:"",t,!1,r.schema),t.embed&&(e.source===t.source||(a=t.source,h(q,a)))?Z(r,t.embed):J(r,t).then((e=>{Z(r,e.html)})).catch(X(r))})(r.get(),o,e),t.close()},onChange:(t,o)=>{switch(o.name){case"source":((t,r)=>{const o=Q(r.getData(),"source");t.source!==o.source&&(Y(u,e)({url:o.source,html:""}),J(e,o).then(Y(u,e)).catch(X(e)))})(r.get(),t);break;case"embed":(t=>{var r;const o=Q(t.getData()),a=T(null!==(r=o.embed)&&void 0!==r?r:"",e.schema);t.setData(V(a))})(t);break;case"dimensions":case"altsource":case"poster":((t,r,o)=>{const a=Q(t.getData(),r),s=te(o,a)&&k(e)?{...a,embed:""}:a,i=G(e,s);t.setData(V({...s,embed:i}))})(t,o.name,r.get())}r.set(Q(t.getData()))},initialData:o})};var oe=tinymce.util.Tools.resolve("tinymce.Env");const ae=e=>{const t=e.name;return"iframe"===t||"video"===t||"audio"===t},se=(e,t,r,o=null)=>{const a=e.attr(r);return s(a)?a:h(t,r)?null:o},ie=(e,t,r)=>{const o="img"===t.name||"video"===e.name,a=o?"300":null,s="audio"===e.name?"30":"150",i=o?s:null;t.attr({width:se(e,r,"width",a),height:se(e,r,"height",i)})},ne=(e,t)=>{const r=t.name,o=new D("img",1);return ce(e,t,o),ie(t,o,{}),o.attr({style:t.attr("style"),src:oe.transparentSrc,"data-mce-object":r,class:"mce-object mce-object-"+r}),o},le=(e,t)=>{var r;const o=t.name,a=new D("span",1);a.attr({contentEditable:"false",style:t.attr("style"),"data-mce-object":o,class:"mce-preview-object mce-object-"+o}),ce(e,t,a);const i=e.dom.parseStyle(null!==(r=t.attr("style"))&&void 0!==r?r:""),n=new D(o,1);if(ie(t,n,i),n.attr({src:t.attr("src"),style:t.attr("style"),class:t.attr("class")}),"iframe"===o)n.attr({allowfullscreen:t.attr("allowfullscreen"),frameborder:"0",sandbox:t.attr("sandbox"),referrerpolicy:t.attr("referrerpolicy")});else{l(["controls","crossorigin","currentTime","loop","muted","poster","preload"],(e=>{n.attr(e,t.attr(e))}));const r=a.attr("data-mce-html");s(r)&&((e,t,r,o)=>{const a=M(e.schema).parse(o,{context:t});for(;a.firstChild;)r.append(a.firstChild)})(e,o,n,unescape(r))}const c=new D("span",1);return c.attr("class","mce-shim"),a.append(n),a.append(c),a},ce=(e,t,r)=>{var o;const a=null!==(o=t.attributes)&&void 0!==o?o:[];let s=a.length;for(;s--;){const t=a[s].name;let o=a[s].value;"width"===t||"height"===t||"style"===t||(n="data-mce-",(i=t).length>=9&&i.substr(0,9)===n)||("data"!==t&&"src"!==t||(o=e.convertURL(o,t)),r.attr("data-mce-p-"+t,o))}var i,n;const c=F({inner:!0},e.schema),m=new D("div",1);l(t.children(),(e=>m.append(e)));const u=c.serialize(m);u&&(r.attr("data-mce-html",escape(u)),r.empty())},me=e=>{const t=e.attr("class");return r(t)&&/\btiny-pageembed\b/.test(t)},ue=e=>{let t=e;for(;t=t.parent;)if(t.attr("data-ephox-embed-iri")||me(t))return!0;return!1},de=(e,t,r)=>{const o=(0,e.options.get)("xss_sanitization"),a=f(e);return M(e.schema,{sanitize:o,validate:a}).parse(r,{context:t})},he=e=>t=>{const r=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",r),r(),()=>{e.off("NodeChange",r)}};e.add("media",(e=>((e=>{const t=e.options.register;t("audio_template_callback",{processor:"function"}),t("video_template_callback",{processor:"function"}),t("iframe_template_callback",{processor:"function"}),t("media_live_embeds",{processor:"boolean",default:!0}),t("media_filter_html",{processor:"boolean",default:!0}),t("media_url_resolver",{processor:"function"}),t("media_alt_source",{processor:"boolean",default:!0}),t("media_poster",{processor:"boolean",default:!0}),t("media_dimensions",{processor:"boolean",default:!0})})(e),(e=>{e.addCommand("mceMedia",(()=>{re(e)}))})(e),(e=>{const t=()=>e.execCommand("mceMedia");e.ui.registry.addToggleButton("media",{tooltip:"Insert/edit media",icon:"embed",onAction:t,onSetup:t=>{const r=e.selection;t.setActive(W(r.getNode()));const o=r.selectorChangedWithUnbind("img[data-mce-object],span[data-mce-object],div[data-ephox-embed-iri]",t.setActive).unbind,a=he(e)(t);return()=>{o(),a()}}}),e.ui.registry.addMenuItem("media",{icon:"embed",text:"Media...",onAction:t,onSetup:he(e)})})(e),(e=>{e.on("ResolveName",(e=>{let t;1===e.target.nodeType&&(t=e.target.getAttribute("data-mce-object"))&&(e.name=t)}))})(e),(e=>{e.on("PreInit",(()=>{const{schema:t,serializer:r,parser:o}=e,a=t.getBoolAttrs();l("webkitallowfullscreen mozallowfullscreen".split(" "),(e=>{a[e]={}})),((e,t)=>{const r=m(e);for(let o=0,a=r.length;o{const o=t.getElementRule(r);o&&l(e,(e=>{o.attributes[e]={},o.attributesOrder.push(e)}))})),o.addNodeFilter("iframe,video,audio,object,embed",(e=>t=>{let r,o=t.length;for(;o--;)r=t[o],r.parent&&(r.parent.attr("data-mce-object")||(ae(r)&&v(e)?ue(r)||r.replace(le(e,r)):ue(r)||r.replace(ne(e,r))))})(e)),r.addAttributeFilter("data-mce-object",((t,r)=>{var o;let a=t.length;for(;a--;){const s=t[a];if(!s.parent)continue;const i=s.attr(r),n=new D(i,1);if("audio"!==i){const e=s.attr("class");e&&-1!==e.indexOf("mce-preview-object")&&s.firstChild?n.attr({width:s.firstChild.attr("width"),height:s.firstChild.attr("height")}):n.attr({width:s.attr("width"),height:s.attr("height")})}n.attr({style:s.attr("style")});const c=null!==(o=s.attributes)&&void 0!==o?o:[];let m=c.length;for(;m--;){const e=c[m].name;0===e.indexOf("data-mce-p-")&&n.attr(e.substr(11),c[m].value)}const u=s.attr("data-mce-html");if(u){const t=de(e,i,unescape(u));l(t.children(),(e=>n.append(e)))}s.replace(n)}}))})),e.on("SetContent",(()=>{const t=e.dom;l(t.select("span.mce-preview-object"),(e=>{0===t.select("span.mce-shim",e).length&&t.add(e,"span",{class:"mce-shim"})}))}))})(e),(e=>{e.on("mousedown",(t=>{const r=e.dom.getParent(t.target,".mce-preview-object");r&&"2"===e.dom.getAttrib(r,"data-mce-selected")&&t.stopImmediatePropagation()})),e.on("click keyup touchend",(()=>{const t=e.selection.getNode();t&&e.dom.hasClass(t,"mce-preview-object")&&e.dom.getAttrib(t,"data-mce-selected")&&t.setAttribute("data-mce-selected","2")})),e.on("ObjectResized",(t=>{const r=t.target;if(r.getAttribute("data-mce-object")){let o=r.getAttribute("data-mce-html");o&&(o=unescape(o),r.setAttribute("data-mce-html",escape(U(o,{width:String(t.width),height:String(t.height)},!1,e.schema))))}}))})(e),(e=>({showDialog:()=>{re(e)}}))(e))))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/nonbreaking/plugin.min.js b/src/main/webapp/content/tinymce/plugins/nonbreaking/plugin.min.js new file mode 100644 index 0000000..048e6d7 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/nonbreaking/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=n=>e=>typeof e===n,o=e("boolean"),a=e("number"),t=n=>e=>e.options.get(n),i=t("nonbreaking_force_tab"),s=t("nonbreaking_wrap"),r=(n,e)=>{let o="";for(let a=0;a{const o=s(n)||n.plugins.visualchars?`${r(" ",e)}`:r(" ",e);n.undoManager.transact((()=>n.insertContent(o)))};var l=tinymce.util.Tools.resolve("tinymce.util.VK");const u=n=>e=>{const o=()=>{e.setEnabled(n.selection.isEditable())};return n.on("NodeChange",o),o(),()=>{n.off("NodeChange",o)}};n.add("nonbreaking",(n=>{(n=>{const e=n.options.register;e("nonbreaking_force_tab",{processor:n=>o(n)?{value:n?3:0,valid:!0}:a(n)?{value:n,valid:!0}:{valid:!1,message:"Must be a boolean or number."},default:!1}),e("nonbreaking_wrap",{processor:"boolean",default:!0})})(n),(n=>{n.addCommand("mceNonBreaking",(()=>{c(n,1)}))})(n),(n=>{const e=()=>n.execCommand("mceNonBreaking");n.ui.registry.addButton("nonbreaking",{icon:"non-breaking",tooltip:"Nonbreaking space",onAction:e,onSetup:u(n)}),n.ui.registry.addMenuItem("nonbreaking",{icon:"non-breaking",text:"Nonbreaking space",onAction:e,onSetup:u(n)})})(n),(n=>{const e=i(n);e>0&&n.on("keydown",(o=>{if(o.keyCode===l.TAB&&!o.isDefaultPrevented()){if(o.shiftKey)return;o.preventDefault(),o.stopImmediatePropagation(),c(n,e)}}))})(n)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/pagebreak/plugin.min.js b/src/main/webapp/content/tinymce/plugins/pagebreak/plugin.min.js new file mode 100644 index 0000000..b498342 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/pagebreak/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env");const t=e=>a=>a.options.get(e),n=t("pagebreak_separator"),o=t("pagebreak_split_block"),r="mce-pagebreak",s=e=>{const t=``;return e?`

    ${t}

    `:t},c=e=>a=>{const t=()=>{a.setEnabled(e.selection.isEditable())};return e.on("NodeChange",t),t(),()=>{e.off("NodeChange",t)}};e.add("pagebreak",(e=>{(e=>{const a=e.options.register;a("pagebreak_separator",{processor:"string",default:"\x3c!-- pagebreak --\x3e"}),a("pagebreak_split_block",{processor:"boolean",default:!1})})(e),(e=>{e.addCommand("mcePageBreak",(()=>{e.insertContent(s(o(e)))}))})(e),(e=>{const a=()=>e.execCommand("mcePageBreak");e.ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:a,onSetup:c(e)}),e.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:a,onSetup:c(e)})})(e),(e=>{const a=n(e),t=()=>o(e),c=new RegExp(a.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,(e=>"\\"+e)),"gi");e.on("BeforeSetContent",(e=>{e.content=e.content.replace(c,s(t()))})),e.on("PreInit",(()=>{e.serializer.addNodeFilter("img",(n=>{let o,s,c=n.length;for(;c--;)if(o=n[c],s=o.attr("class"),s&&-1!==s.indexOf(r)){const n=o.parent;if(n&&e.schema.getBlockElements()[n.name]&&t()){n.type=3,n.value=a,n.raw=!0,o.remove();continue}o.type=3,o.value=a,o.raw=!0}}))}))})(e),(e=>{e.on("ResolveName",(a=>{"IMG"===a.target.nodeName&&e.dom.hasClass(a.target,r)&&(a.name="pagebreak")}))})(e)}))}(); \ No newline at end of file diff --git a/src/main/webapp/content/tinymce/plugins/preview/plugin.min.js b/src/main/webapp/content/tinymce/plugins/preview/plugin.min.js new file mode 100644 index 0000000..41e9891 --- /dev/null +++ b/src/main/webapp/content/tinymce/plugins/preview/plugin.min.js @@ -0,0 +1,4 @@ +/** + * TinyMCE version 7.8.0 (TBD) + */ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>undefined===e;class r{constructor(e,t){this.tag=e,this.value=t}static some(e){return new r(!0,e)}static none(){return r.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?r.some(e(this.value)):r.none()}bind(e){return this.tag?e(this.value):r.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:r.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return null==e?r.none():r.some(e)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}r.singletonNone=new r(!1);const n=e=>()=>e,s=n(!1),i=(e,t)=>((e,t,n)=>{for(let s=0,i=e.length;sa(0,0),a=(e,t)=>({major:e,minor:t}),c={nu:a,detect:(e,t)=>{const r=String(t).toLowerCase();return 0===e.length?o():((e,t)=>{const r=((e,t)=>{for(let r=0;rNumber(t.replace(r,"$"+e));return a(n(1),n(2))})(e,r)},unknown:o},u=(e,t)=>{const r=String(t).toLowerCase();return i(e,(e=>e.search(r)))},d=(e,r,n=0,s)=>{const i=e.indexOf(r,n);return-1!==i&&(!!t(s)||i+r.length<=s)},l=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,h=e=>t=>d(t,e),m=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:e=>d(e,"edge/")&&d(e,"chrome")&&d(e,"safari")&&d(e,"applewebkit")},{name:"Chromium",brand:"Chromium",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,l],search:e=>d(e,"chrome")&&!d(e,"chromeframe")},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:e=>d(e,"msie")||d(e,"trident")},{name:"Opera",versionRegexes:[l,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:h("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:h("firefox")},{name:"Safari",versionRegexes:[l,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:e=>(d(e,"safari")||d(e,"mobile/"))&&d(e,"applewebkit")}],v=[{name:"Windows",search:h("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:e=>d(e,"iphone")||d(e,"ipad"),versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:h("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"macOS",search:h("mac os x"),versionRegexes:[/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:h("linux"),versionRegexes:[]},{name:"Solaris",search:h("sunos"),versionRegexes:[]},{name:"FreeBSD",search:h("freebsd"),versionRegexes:[]},{name:"ChromeOS",search:h("cros"),versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/]}],g={browsers:n(m),oses:n(v)},p="Edge",w="Chromium",f="Opera",x="Firefox",S="Safari",y=e=>{const t=e.current,r=e.version,n=e=>()=>t===e;return{current:t,version:r,isEdge:n(p),isChromium:n(w),isIE:n("IE"),isOpera:n(f),isFirefox:n(x),isSafari:n(S)}},b=()=>y({current:void 0,version:c.unknown()}),O=y,R=(n(p),n(w),n("IE"),n(f),n(x),n(S),"Windows"),C="Android",A="Linux",k="macOS",D="Solaris",E="FreeBSD",I="ChromeOS",P=e=>{const t=e.current,r=e.version,n=e=>()=>t===e;return{current:t,version:r,isWindows:n(R),isiOS:n("iOS"),isAndroid:n(C),isMacOS:n(k),isLinux:n(A),isSolaris:n(D),isFreeBSD:n(E),isChromeOS:n(I)}},T=()=>P({current:void 0,version:c.unknown()}),_=P,B=(n(R),n("iOS"),n(C),n(A),n(k),n(D),n(E),n(I),(e,t,s)=>{const o=g.browsers(),a=g.oses(),d=t.bind((e=>((e,t)=>((e,t)=>{for(let r=0;r{const r=t.brand.toLowerCase();return i(e,(e=>{var t;return r===(null===(t=e.brand)||void 0===t?void 0:t.toLowerCase())})).map((e=>({current:e.name,version:c.nu(parseInt(t.version,10),0)})))})))(o,e))).orThunk((()=>((e,t)=>u(e,t).map((e=>{const r=c.detect(e.versionRegexes,t);return{current:e.name,version:r}})))(o,e))).fold(b,O),l=((e,t)=>u(e,t).map((e=>{const r=c.detect(e.versionRegexes,t);return{current:e.name,version:r}})))(a,e).fold(T,_),h=((e,t,r,s)=>{const i=e.isiOS()&&!0===/ipad/i.test(r),o=e.isiOS()&&!i,a=e.isiOS()||e.isAndroid(),c=a||s("(pointer:coarse)"),u=i||!o&&a&&s("(min-device-width:768px)"),d=o||a&&!u,l=t.isSafari()&&e.isiOS()&&!1===/safari/i.test(r),h=!d&&!u&&!l;return{isiPad:n(i),isiPhone:n(o),isTablet:n(u),isPhone:n(d),isTouch:n(c),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:n(l),isDesktop:n(h)}})(l,d,e,s);return{browser:d,os:l,deviceType:h}}),L=e=>window.matchMedia(e).matches;let N=(e=>{let t,r=!1;return(...n)=>(r||(r=!0,t=e.apply(null,n)),t)})((()=>B(window.navigator.userAgent,r.from(window.navigator.userAgentData),L)));const F=()=>N();var M=tinymce.util.Tools.resolve("tinymce.util.Tools");const $=e=>t=>t.options.get(e),W=$("content_style"),U=$("content_css_cors"),K=$("body_class"),j=$("body_id"),V=e=>{const t=(e=>{var t;let r="";const n=e.dom.encode,s=null!==(t=W(e))&&void 0!==t?t:"";r+=``;const i=U(e)?' crossorigin="anonymous"':"";M.each(e.contentCSS,(t=>{r+='"})),s&&(r+='");const o=j(e),a=K(e),c=e.getBody().dir,u=c?' dir="'+n(c)+'"':"";return""+r+'"+e.getContent()+(()=>{const e=F().os.isMacOS()||F().os.isiOS();return` + + diff --git a/src/main/webapp/index.html.old b/src/main/webapp/index.html.old new file mode 100644 index 0000000..4c148cd --- /dev/null +++ b/src/main/webapp/index.html.old @@ -0,0 +1,119 @@ + + + + + + resilient + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + + diff --git a/src/main/webapp/main.ts b/src/main/webapp/main.ts new file mode 100644 index 0000000..73c3a0e --- /dev/null +++ b/src/main/webapp/main.ts @@ -0,0 +1 @@ +import('./bootstrap').catch(err => console.error(err)); diff --git a/src/main/webapp/manifest.webapp b/src/main/webapp/manifest.webapp new file mode 100644 index 0000000..90f2c31 --- /dev/null +++ b/src/main/webapp/manifest.webapp @@ -0,0 +1,31 @@ +{ + "name": "Resilient", + "short_name": "Resilient", + "icons": [ + { + "src": "./content/images/jhipster_family_member_1_head-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./content/images/jhipster_family_member_1_head-256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "./content/images/jhipster_family_member_1_head-384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "./content/images/jhipster_family_member_1_head-512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#000000", + "background_color": "#e0e0e0", + "start_url": ".", + "display": "standalone", + "orientation": "portrait" +} diff --git a/src/main/webapp/robots.txt b/src/main/webapp/robots.txt new file mode 100644 index 0000000..361ff60 --- /dev/null +++ b/src/main/webapp/robots.txt @@ -0,0 +1,10 @@ +# robotstxt.org/ + +User-agent: * +Disallow: /api/account +Disallow: /api/account/change-password +Disallow: /api/account/sessions +Disallow: /api/logs/ +Disallow: /api/users/ +Disallow: /management/ +Disallow: /v3/api-docs/ diff --git a/src/main/webapp/swagger-ui/index.html b/src/main/webapp/swagger-ui/index.html new file mode 100644 index 0000000..79d52f0 --- /dev/null +++ b/src/main/webapp/swagger-ui/index.html @@ -0,0 +1,115 @@ + + + + + resilient - Swagger UI + + + + + + + +
    + + + + + + + + diff --git a/src/test/java/com/oguerreiro/resilient/GeneratePasswordBCript.java b/src/test/java/com/oguerreiro/resilient/GeneratePasswordBCript.java new file mode 100644 index 0000000..21de58b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/GeneratePasswordBCript.java @@ -0,0 +1,57 @@ +package com.oguerreiro.resilient; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class GeneratePasswordBCript { + + /** + * Generates a BCrypt password encoded, to use in user direct insert in database + * + * @param args + */ + public static void main(String[] args) { + List passwords = new ArrayList(); + passwords.add("cpss"); + passwords.add("cbernardo"); + passwords.add("chicau"); + passwords.add("paulap"); + passwords.add("susana.santos"); + passwords.add("manuel.salvador"); + passwords.add("vitoria.ferreira"); + passwords.add("pfluz"); + passwords.add("sribeiro"); + passwords.add("patricia.ferreira"); + passwords.add("jose.malcato"); + passwords.add("maria.ravara"); + + String pwdAppend = "#"; + List numbers = GeneratePasswordBCript.getRandom(passwords.size()); + + for (String user : passwords) { + String pwdRandom = numbers.get(0); + numbers.remove(0); + String pwd = user + pwdAppend + pwdRandom; + String pwdCrypt = new BCryptPasswordEncoder().encode(pwd); + System.out.println(user + " :: " + pwd + " = " + pwdCrypt); + } + + } + + private static List getRandom(int size) { + Set numbers = new HashSet<>(); + + while (numbers.size() < size) { + int random = ThreadLocalRandom.current().nextInt(10000, 100000); // 10000 to 99999 + numbers.add(String.valueOf(random)); + } + + // Convert to a list to allow indexed access + return new ArrayList<>(numbers); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/IntegrationTest.java b/src/test/java/com/oguerreiro/resilient/IntegrationTest.java new file mode 100644 index 0000000..ea754f4 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/IntegrationTest.java @@ -0,0 +1,20 @@ +package com.oguerreiro.resilient; + +import com.oguerreiro.resilient.config.AsyncSyncConfiguration; +import com.oguerreiro.resilient.config.EmbeddedSQL; +import com.oguerreiro.resilient.config.JacksonConfiguration; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Base composite annotation for integration tests. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest(classes = { ResilientApp.class, JacksonConfiguration.class, AsyncSyncConfiguration.class }) +@EmbeddedSQL +public @interface IntegrationTest { +} diff --git a/src/test/java/com/oguerreiro/resilient/OutputDataSumTest.java b/src/test/java/com/oguerreiro/resilient/OutputDataSumTest.java new file mode 100644 index 0000000..88dbef2 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/OutputDataSumTest.java @@ -0,0 +1,36 @@ +package com.oguerreiro.resilient; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.OutputDataRepository; +import com.oguerreiro.resilient.repository.PeriodVersionRepository; + +@ActiveProfiles("testdev") // Replace "dev" with your desired profile +@SpringBootTest +class OutputDataSumTest { + @Autowired + private OutputDataRepository outputDataRepository; + + @Autowired + private OrganizationRepository organizationRepository; + + @Autowired + private PeriodVersionRepository periodVersionRepository; + + @Test + void sumLikeQuery() { + Organization org = organizationRepository.getReferenceById(4L); + PeriodVersion periodVersion = periodVersionRepository.getReferenceById(1511L); + BigDecimal sum = outputDataRepository.sumAllOutputsFor(org, periodVersion, "5301%"); + + System.out.println(sum.toString()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/TechnicalStructureTest.java b/src/test/java/com/oguerreiro/resilient/TechnicalStructureTest.java new file mode 100644 index 0000000..7f7bc95 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/TechnicalStructureTest.java @@ -0,0 +1,38 @@ +package com.oguerreiro.resilient; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.library.Architectures.layeredArchitecture; + +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +@AnalyzeClasses(packagesOf = ResilientApp.class, importOptions = DoNotIncludeTests.class) +class TechnicalStructureTest { + + // prettier-ignore + @ArchTest + static final ArchRule respectsTechnicalArchitectureLayers = layeredArchitecture() + .consideringAllDependencies() + .layer("Config").definedBy("..config..") + .layer("Web").definedBy("..web..") + .optionalLayer("Service").definedBy("..service..") + .layer("Security").definedBy("..security..") + .optionalLayer("Persistence").definedBy("..repository..") + .layer("Domain").definedBy("..domain..") + + .whereLayer("Config").mayNotBeAccessedByAnyLayer() + .whereLayer("Web").mayOnlyBeAccessedByLayers("Config") + .whereLayer("Service").mayOnlyBeAccessedByLayers("Web", "Config") + .whereLayer("Security").mayOnlyBeAccessedByLayers("Config", "Service", "Web") + .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service", "Security", "Web", "Config") + .whereLayer("Domain").mayOnlyBeAccessedByLayers("Persistence", "Service", "Security", "Web", "Config") + + .ignoreDependency(belongToAnyOf(ResilientApp.class), alwaysTrue()) + .ignoreDependency(alwaysTrue(), belongToAnyOf( + com.oguerreiro.resilient.config.Constants.class, + com.oguerreiro.resilient.config.ApplicationProperties.class + )); +} diff --git a/src/test/java/com/oguerreiro/resilient/config/AsyncSyncConfiguration.java b/src/test/java/com/oguerreiro/resilient/config/AsyncSyncConfiguration.java new file mode 100644 index 0000000..e11926b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/AsyncSyncConfiguration.java @@ -0,0 +1,15 @@ +package com.oguerreiro.resilient.config; + +import java.util.concurrent.Executor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SyncTaskExecutor; + +@Configuration +public class AsyncSyncConfiguration { + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + return new SyncTaskExecutor(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/config/CRLFLogConverterTest.java b/src/test/java/com/oguerreiro/resilient/config/CRLFLogConverterTest.java new file mode 100644 index 0000000..8609965 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/CRLFLogConverterTest.java @@ -0,0 +1,133 @@ +package com.oguerreiro.resilient.config; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import org.springframework.boot.ansi.AnsiColor; +import org.springframework.boot.ansi.AnsiElement; + +class CRLFLogConverterTest { + + @Test + void transformShouldReturnInputStringWhenMarkerListIsEmpty() { + ILoggingEvent event = mock(ILoggingEvent.class); + when(event.getMarkerList()).thenReturn(null); + when(event.getLoggerName()).thenReturn("org.hibernate.example.Logger"); + String input = "Test input string"; + CRLFLogConverter converter = new CRLFLogConverter(); + + String result = converter.transform(event, input); + + assertEquals(input, result); + } + + @Test + void transformShouldReturnInputStringWhenMarkersContainCRLFSafeMarker() { + ILoggingEvent event = mock(ILoggingEvent.class); + Marker marker = MarkerFactory.getMarker("CRLF_SAFE"); + List markers = Collections.singletonList(marker); + when(event.getMarkerList()).thenReturn(markers); + String input = "Test input string"; + CRLFLogConverter converter = new CRLFLogConverter(); + + String result = converter.transform(event, input); + + assertEquals(input, result); + } + + @Test + void transformShouldReturnInputStringWhenMarkersNotContainCRLFSafeMarker() { + ILoggingEvent event = mock(ILoggingEvent.class); + Marker marker = MarkerFactory.getMarker("CRLF_NOT_SAFE"); + List markers = Collections.singletonList(marker); + when(event.getMarkerList()).thenReturn(markers); + when(event.getLoggerName()).thenReturn("org.hibernate.example.Logger"); + String input = "Test input string"; + CRLFLogConverter converter = new CRLFLogConverter(); + + String result = converter.transform(event, input); + + assertEquals(input, result); + } + + @Test + void transformShouldReturnInputStringWhenLoggerIsSafe() { + ILoggingEvent event = mock(ILoggingEvent.class); + when(event.getLoggerName()).thenReturn("org.hibernate.example.Logger"); + String input = "Test input string"; + CRLFLogConverter converter = new CRLFLogConverter(); + + String result = converter.transform(event, input); + + assertEquals(input, result); + } + + @Test + void transformShouldReplaceNewlinesAndCarriageReturnsWithUnderscoreWhenMarkersDoNotContainCRLFSafeMarkerAndLoggerIsNotSafe() { + ILoggingEvent event = mock(ILoggingEvent.class); + List markers = Collections.emptyList(); + when(event.getMarkerList()).thenReturn(markers); + when(event.getLoggerName()).thenReturn("com.mycompany.myapp.example.Logger"); + String input = "Test\ninput\rstring"; + CRLFLogConverter converter = new CRLFLogConverter(); + + String result = converter.transform(event, input); + + assertEquals("Test_input_string", result); + } + + @Test + void transformShouldReplaceNewlinesAndCarriageReturnsWithAnsiStringWhenMarkersDoNotContainCRLFSafeMarkerAndLoggerIsNotSafeAndAnsiElementIsNotNull() { + ILoggingEvent event = mock(ILoggingEvent.class); + List markers = Collections.emptyList(); + when(event.getMarkerList()).thenReturn(markers); + when(event.getLoggerName()).thenReturn("com.mycompany.myapp.example.Logger"); + String input = "Test\ninput\rstring"; + CRLFLogConverter converter = new CRLFLogConverter(); + converter.setOptionList(List.of("red")); + + String result = converter.transform(event, input); + + assertEquals("Test_input_string", result); + } + + @Test + void isLoggerSafeShouldReturnTrueWhenLoggerNameStartsWithSafeLogger() { + ILoggingEvent event = mock(ILoggingEvent.class); + when(event.getLoggerName()).thenReturn("org.springframework.boot.autoconfigure.example.Logger"); + CRLFLogConverter converter = new CRLFLogConverter(); + + boolean result = converter.isLoggerSafe(event); + + assertTrue(result); + } + + @Test + void isLoggerSafeShouldReturnFalseWhenLoggerNameDoesNotStartWithSafeLogger() { + ILoggingEvent event = mock(ILoggingEvent.class); + when(event.getLoggerName()).thenReturn("com.mycompany.myapp.example.Logger"); + CRLFLogConverter converter = new CRLFLogConverter(); + + boolean result = converter.isLoggerSafe(event); + + assertFalse(result); + } + + @Test + void testToAnsiString() { + CRLFLogConverter cut = new CRLFLogConverter(); + AnsiElement ansiElement = AnsiColor.RED; + + String result = cut.toAnsiString("input", ansiElement); + + assertThat(result).isEqualTo("input"); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/config/EmbeddedSQL.java b/src/test/java/com/oguerreiro/resilient/config/EmbeddedSQL.java new file mode 100644 index 0000000..ee9e7ad --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/EmbeddedSQL.java @@ -0,0 +1,11 @@ +package com.oguerreiro.resilient.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface EmbeddedSQL { +} diff --git a/src/test/java/com/oguerreiro/resilient/config/MysqlTestContainer.java b/src/test/java/com/oguerreiro/resilient/config/MysqlTestContainer.java new file mode 100644 index 0000000..6cc075a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/MysqlTestContainer.java @@ -0,0 +1,41 @@ +package com.oguerreiro.resilient.config; + +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +public class MysqlTestContainer implements SqlTestContainer { + + private static final Logger log = LoggerFactory.getLogger(MysqlTestContainer.class); + + private MySQLContainer mysqlContainer; + + @Override + public void destroy() { + if (null != mysqlContainer && mysqlContainer.isRunning()) { + mysqlContainer.stop(); + } + } + + @Override + public void afterPropertiesSet() { + if (null == mysqlContainer) { + mysqlContainer = new MySQLContainer<>("mysql:8.4.0") + .withDatabaseName("resilient") + .withTmpFs(Collections.singletonMap("/testtmpfs", "rw")) + .withLogConsumer(new Slf4jLogConsumer(log)) + .withReuse(true); + } + if (!mysqlContainer.isRunning()) { + mysqlContainer.start(); + } + } + + @Override + public JdbcDatabaseContainer getTestContainer() { + return mysqlContainer; + } +} diff --git a/src/test/java/com/oguerreiro/resilient/config/SpringBootTestClassOrderer.java b/src/test/java/com/oguerreiro/resilient/config/SpringBootTestClassOrderer.java new file mode 100644 index 0000000..75514d4 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/SpringBootTestClassOrderer.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.config; + +import com.oguerreiro.resilient.IntegrationTest; +import java.util.Comparator; +import org.junit.jupiter.api.ClassDescriptor; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.ClassOrdererContext; + +public class SpringBootTestClassOrderer implements ClassOrderer { + + @Override + public void orderClasses(ClassOrdererContext context) { + context.getClassDescriptors().sort(Comparator.comparingInt(SpringBootTestClassOrderer::getOrder)); + } + + private static int getOrder(ClassDescriptor classDescriptor) { + if (classDescriptor.findAnnotation(IntegrationTest.class).isPresent()) { + return 2; + } + return 1; + } +} diff --git a/src/test/java/com/oguerreiro/resilient/config/SqlTestContainer.java b/src/test/java/com/oguerreiro/resilient/config/SqlTestContainer.java new file mode 100644 index 0000000..ff20ee6 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/SqlTestContainer.java @@ -0,0 +1,9 @@ +package com.oguerreiro.resilient.config; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.testcontainers.containers.JdbcDatabaseContainer; + +public interface SqlTestContainer extends InitializingBean, DisposableBean { + JdbcDatabaseContainer getTestContainer(); +} diff --git a/src/test/java/com/oguerreiro/resilient/config/SqlTestContainersSpringContextCustomizerFactory.java b/src/test/java/com/oguerreiro/resilient/config/SqlTestContainersSpringContextCustomizerFactory.java new file mode 100644 index 0000000..f9aebf3 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/SqlTestContainersSpringContextCustomizerFactory.java @@ -0,0 +1,68 @@ +package com.oguerreiro.resilient.config; + +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; + +public class SqlTestContainersSpringContextCustomizerFactory implements ContextCustomizerFactory { + + private Logger log = LoggerFactory.getLogger(SqlTestContainersSpringContextCustomizerFactory.class); + + private static SqlTestContainer prodTestContainer; + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { + return new ContextCustomizer() { + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + TestPropertyValues testValues = TestPropertyValues.empty(); + EmbeddedSQL sqlAnnotation = AnnotatedElementUtils.findMergedAnnotation(testClass, EmbeddedSQL.class); + if (null != sqlAnnotation) { + log.debug("detected the EmbeddedSQL annotation on class {}", testClass.getName()); + log.info("Warming up the sql database"); + if (null == prodTestContainer) { + try { + Class containerClass = (Class) Class.forName( + this.getClass().getPackageName() + ".MysqlTestContainer" + ); + prodTestContainer = beanFactory.createBean(containerClass); + beanFactory.registerSingleton(containerClass.getName(), prodTestContainer); + /** + * ((DefaultListableBeanFactory)beanFactory).registerDisposableBean(containerClass.getName(), prodTestContainer); + */ + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + testValues = testValues.and( + "spring.datasource.url=" + + prodTestContainer.getTestContainer().getJdbcUrl() + + "?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true" + ); + testValues = testValues.and("spring.datasource.username=" + prodTestContainer.getTestContainer().getUsername()); + testValues = testValues.and("spring.datasource.password=" + prodTestContainer.getTestContainer().getPassword()); + } + testValues.applyTo(context); + } + + @Override + public int hashCode() { + return SqlTestContainer.class.getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return this.hashCode() == obj.hashCode(); + } + }; + } +} diff --git a/src/test/java/com/oguerreiro/resilient/config/StaticResourcesWebConfigurerTest.java b/src/test/java/com/oguerreiro/resilient/config/StaticResourcesWebConfigurerTest.java new file mode 100644 index 0000000..0ed29b2 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/StaticResourcesWebConfigurerTest.java @@ -0,0 +1,76 @@ +package com.oguerreiro.resilient.config; + +import static com.oguerreiro.resilient.config.StaticResourcesWebConfiguration.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.CacheControl; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import tech.jhipster.config.JHipsterDefaults; +import tech.jhipster.config.JHipsterProperties; + +class StaticResourcesWebConfigurerTest { + + public static final int MAX_AGE_TEST = 5; + public StaticResourcesWebConfiguration staticResourcesWebConfiguration; + private ResourceHandlerRegistry resourceHandlerRegistry; + private MockServletContext servletContext; + private WebApplicationContext applicationContext; + private JHipsterProperties props; + + @BeforeEach + void setUp() { + servletContext = spy(new MockServletContext()); + applicationContext = mock(WebApplicationContext.class); + resourceHandlerRegistry = spy(new ResourceHandlerRegistry(applicationContext, servletContext)); + props = new JHipsterProperties(); + staticResourcesWebConfiguration = spy(new StaticResourcesWebConfiguration(props)); + } + + @Test + void shouldAppendResourceHandlerAndInitializeIt() { + staticResourcesWebConfiguration.addResourceHandlers(resourceHandlerRegistry); + + verify(resourceHandlerRegistry, times(1)).addResourceHandler(RESOURCE_PATHS); + verify(staticResourcesWebConfiguration, times(1)).initializeResourceHandler(any(ResourceHandlerRegistration.class)); + for (String testingPath : RESOURCE_PATHS) { + assertThat(resourceHandlerRegistry.hasMappingForPattern(testingPath)).isTrue(); + } + } + + @Test + void shouldInitializeResourceHandlerWithCacheControlAndLocations() { + CacheControl ccExpected = CacheControl.maxAge(5, TimeUnit.DAYS).cachePublic(); + when(staticResourcesWebConfiguration.getCacheControl()).thenReturn(ccExpected); + ResourceHandlerRegistration resourceHandlerRegistration = spy(new ResourceHandlerRegistration(RESOURCE_PATHS)); + + staticResourcesWebConfiguration.initializeResourceHandler(resourceHandlerRegistration); + + verify(staticResourcesWebConfiguration, times(1)).getCacheControl(); + verify(resourceHandlerRegistration, times(1)).setCacheControl(ccExpected); + verify(resourceHandlerRegistration, times(1)).addResourceLocations(RESOURCE_LOCATIONS); + } + + @Test + void shouldCreateCacheControlBasedOnJhipsterDefaultProperties() { + CacheControl cacheExpected = CacheControl.maxAge(JHipsterDefaults.Http.Cache.timeToLiveInDays, TimeUnit.DAYS).cachePublic(); + assertThat(staticResourcesWebConfiguration.getCacheControl()) + .extracting(CacheControl::getHeaderValue) + .isEqualTo(cacheExpected.getHeaderValue()); + } + + @Test + void shouldCreateCacheControlWithSpecificConfigurationInProperties() { + props.getHttp().getCache().setTimeToLiveInDays(MAX_AGE_TEST); + CacheControl cacheExpected = CacheControl.maxAge(MAX_AGE_TEST, TimeUnit.DAYS).cachePublic(); + assertThat(staticResourcesWebConfiguration.getCacheControl()) + .extracting(CacheControl::getHeaderValue) + .isEqualTo(cacheExpected.getHeaderValue()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/config/WebConfigurerTest.java b/src/test/java/com/oguerreiro/resilient/config/WebConfigurerTest.java new file mode 100644 index 0000000..7c5e7cb --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/WebConfigurerTest.java @@ -0,0 +1,132 @@ +package com.oguerreiro.resilient.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import jakarta.servlet.*; +import java.io.File; +import java.util.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; + +/** + * Unit tests for the {@link WebConfigurer} class. + */ +class WebConfigurerTest { + + private WebConfigurer webConfigurer; + + private MockServletContext servletContext; + + private MockEnvironment env; + + private JHipsterProperties props; + + @BeforeEach + public void setup() { + servletContext = spy(new MockServletContext()); + doReturn(mock(FilterRegistration.Dynamic.class)).when(servletContext).addFilter(anyString(), any(Filter.class)); + doReturn(mock(ServletRegistration.Dynamic.class)).when(servletContext).addServlet(anyString(), any(Servlet.class)); + + env = new MockEnvironment(); + props = new JHipsterProperties(); + + webConfigurer = new WebConfigurer(env, props); + } + + @Test + void shouldCustomizeServletContainer() { + env.setActiveProfiles(JHipsterConstants.SPRING_PROFILE_PRODUCTION); + UndertowServletWebServerFactory container = new UndertowServletWebServerFactory(); + webConfigurer.customize(container); + assertThat(container.getMimeMappings().get("abs")).isEqualTo("audio/x-mpeg"); + assertThat(container.getMimeMappings().get("html")).isEqualTo("text/html"); + assertThat(container.getMimeMappings().get("json")).isEqualTo("application/json"); + if (container.getDocumentRoot() != null) { + assertThat(container.getDocumentRoot()).isEqualTo(new File("target/classes/static/")); + } + } + + @Test + void shouldCorsFilterOnApiPath() throws Exception { + props.getCors().setAllowedOrigins(Collections.singletonList("other.domain.com")); + props.getCors().setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); + props.getCors().setAllowedHeaders(Collections.singletonList("*")); + props.getCors().setMaxAge(1800L); + props.getCors().setAllowCredentials(true); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform( + options("/api/test-cors") + .header(HttpHeaders.ORIGIN, "other.domain.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST") + ) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "other.domain.com")) + .andExpect(header().string(HttpHeaders.VARY, "Origin")) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,PUT,DELETE")) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "1800")); + + mockMvc + .perform(get("/api/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "other.domain.com")); + } + + @Test + void shouldCorsFilterOnOtherPath() throws Exception { + props.getCors().setAllowedOrigins(Collections.singletonList("*")); + props.getCors().setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); + props.getCors().setAllowedHeaders(Collections.singletonList("*")); + props.getCors().setMaxAge(1800L); + props.getCors().setAllowCredentials(true); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform(get("/test/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + void shouldCorsFilterDeactivatedForNullAllowedOrigins() throws Exception { + props.getCors().setAllowedOrigins(null); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform(get("/api/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + void shouldCorsFilterDeactivatedForEmptyAllowedOrigins() throws Exception { + props.getCors().setAllowedOrigins(new ArrayList<>()); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform(get("/api/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/config/WebConfigurerTestController.java b/src/test/java/com/oguerreiro/resilient/config/WebConfigurerTestController.java new file mode 100644 index 0000000..55aa7ac --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/WebConfigurerTestController.java @@ -0,0 +1,14 @@ +package com.oguerreiro.resilient.config; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WebConfigurerTestController { + + @GetMapping("/api/test-cors") + public void testCorsOnApiPath() {} + + @GetMapping("/test/test-cors") + public void testCorsOnOtherPath() {} +} diff --git a/src/test/java/com/oguerreiro/resilient/config/timezone/HibernateTimeZoneIT.java b/src/test/java/com/oguerreiro/resilient/config/timezone/HibernateTimeZoneIT.java new file mode 100644 index 0000000..cc3255f --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/config/timezone/HibernateTimeZoneIT.java @@ -0,0 +1,172 @@ +package com.oguerreiro.resilient.config.timezone; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.repository.timezone.DateTimeWrapper; +import com.oguerreiro.resilient.repository.timezone.DateTimeWrapperRepository; +import java.time.*; +import java.time.format.DateTimeFormatter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for verifying the behavior of Hibernate in the context of storing various date and time types across different databases. + * The tests focus on ensuring that the stored values are correctly transformed and stored according to the configured timezone. + * Timezone is environment specific, and can be adjusted according to your needs. + * + * For more context, refer to: + * - GitHub Issue: https://github.com/jhipster/generator-jhipster/issues/22579 + * - Pull Request: https://github.com/jhipster/generator-jhipster/pull/22946 + */ +@IntegrationTest +class HibernateTimeZoneIT { + + @Autowired + private DateTimeWrapperRepository dateTimeWrapperRepository; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Value("${spring.jpa.properties.hibernate.jdbc.time_zone:UTC}") + private String zoneId; + + private DateTimeWrapper dateTimeWrapper; + private DateTimeFormatter dateTimeFormatter; + private DateTimeFormatter timeFormatter; + private DateTimeFormatter offsetTimeFormatter; + private DateTimeFormatter dateFormatter; + + @BeforeEach + public void setup() { + dateTimeWrapper = new DateTimeWrapper(); + dateTimeWrapper.setInstant(Instant.parse("2014-11-12T05:10:00.0Z")); + dateTimeWrapper.setLocalDateTime(LocalDateTime.parse("2014-11-12T07:20:00.0")); + dateTimeWrapper.setOffsetDateTime(OffsetDateTime.parse("2011-12-14T08:30:00.0Z")); + dateTimeWrapper.setZonedDateTime(ZonedDateTime.parse("2011-12-14T08:40:00.0Z")); + dateTimeWrapper.setLocalTime(LocalTime.parse("14:50:00")); + dateTimeWrapper.setOffsetTime(OffsetTime.parse("14:00:00+02:00")); + dateTimeWrapper.setLocalDate(LocalDate.parse("2016-09-10")); + + dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.S").withZone(ZoneId.of(zoneId)); + timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.of(zoneId)); + offsetTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + } + + @Test + @Transactional + void storeInstantWithZoneIdConfigShouldBeStoredOnConfiguredTimeZone() { + dateTimeWrapperRepository.saveAndFlush(dateTimeWrapper); + + String request = generateSqlRequest("instant", dateTimeWrapper.getId()); + SqlRowSet resultSet = jdbcTemplate.queryForRowSet(request); + String expectedValue = dateTimeFormatter.format(dateTimeWrapper.getInstant()); + + assertThatValueFromSqlRowSetIsEqualToExpectedValue(resultSet, expectedValue); + } + + @Test + @Transactional + void storeLocalDateTimeWithZoneIdConfigShouldBeStoredOnConfiguredTimeZone() { + dateTimeWrapperRepository.saveAndFlush(dateTimeWrapper); + + String request = generateSqlRequest("local_date_time", dateTimeWrapper.getId()); + SqlRowSet resultSet = jdbcTemplate.queryForRowSet(request); + String expectedValue = dateTimeWrapper.getLocalDateTime().atZone(ZoneId.systemDefault()).format(dateTimeFormatter); + + assertThatValueFromSqlRowSetIsEqualToExpectedValue(resultSet, expectedValue); + } + + @Test + @Transactional + void storeOffsetDateTimeWithZoneIdConfigShouldBeStoredOnConfiguredTimeZone() { + dateTimeWrapperRepository.saveAndFlush(dateTimeWrapper); + + String request = generateSqlRequest("offset_date_time", dateTimeWrapper.getId()); + SqlRowSet resultSet = jdbcTemplate.queryForRowSet(request); + String expectedValue = dateTimeWrapper.getOffsetDateTime().format(dateTimeFormatter); + + assertThatValueFromSqlRowSetIsEqualToExpectedValue(resultSet, expectedValue); + } + + @Test + @Transactional + void storeZoneDateTimeWithZoneIdConfigShouldBeStoredOnConfiguredTimeZone() { + dateTimeWrapperRepository.saveAndFlush(dateTimeWrapper); + + String request = generateSqlRequest("zoned_date_time", dateTimeWrapper.getId()); + SqlRowSet resultSet = jdbcTemplate.queryForRowSet(request); + String expectedValue = dateTimeWrapper.getZonedDateTime().format(dateTimeFormatter); + + assertThatValueFromSqlRowSetIsEqualToExpectedValue(resultSet, expectedValue); + } + + @Test + @Transactional + void storeLocalTimeWithZoneIdConfigShouldBeStoredOnConfiguredTimeZoneAccordingToHis1stJan1970Value() { + dateTimeWrapperRepository.saveAndFlush(dateTimeWrapper); + + String request = generateSqlRequest("local_time", dateTimeWrapper.getId()); + SqlRowSet resultSet = jdbcTemplate.queryForRowSet(request); + String expectedValue = dateTimeWrapper + .getLocalTime() + .atDate(LocalDate.of(1970, Month.JANUARY, 1)) + .atZone(ZoneId.systemDefault()) + .format(timeFormatter); + + assertThatValueFromSqlRowSetIsEqualToExpectedValue(resultSet, expectedValue); + } + + @Test + @Transactional + void storeOffsetTimeWithZoneIdConfigShouldBeStoredOnConfiguredTimeZoneAccordingToHis1stJan1970Value() { + dateTimeWrapperRepository.saveAndFlush(dateTimeWrapper); + + String request = generateSqlRequest("offset_time", dateTimeWrapper.getId()); + SqlRowSet resultSet = jdbcTemplate.queryForRowSet(request); + String expectedValue = dateTimeWrapper + .getOffsetTime() + // Convert to configured timezone + .withOffsetSameInstant(ZoneId.of(zoneId).getRules().getOffset(Instant.now())) + // Normalize to System TimeZone. + // TODO this behavior looks a bug, refer to https://github.com/jhipster/generator-jhipster/issues/22579. + .withOffsetSameLocal(OffsetDateTime.ofInstant(Instant.EPOCH, ZoneId.systemDefault()).getOffset()) + // Convert the normalized value to configured timezone + .withOffsetSameInstant(ZoneId.of(zoneId).getRules().getOffset(Instant.EPOCH)) + .format(offsetTimeFormatter); + + assertThatValueFromSqlRowSetIsEqualToExpectedValue(resultSet, expectedValue); + } + + @Test + @Transactional + void storeLocalDateWithZoneIdConfigShouldBeStoredWithoutTransformation() { + dateTimeWrapperRepository.saveAndFlush(dateTimeWrapper); + + String request = generateSqlRequest("local_date", dateTimeWrapper.getId()); + SqlRowSet resultSet = jdbcTemplate.queryForRowSet(request); + String expectedValue = dateTimeWrapper.getLocalDate().format(dateFormatter); + + assertThatValueFromSqlRowSetIsEqualToExpectedValue(resultSet, expectedValue); + } + + private String generateSqlRequest(String fieldName, long id) { + return format("SELECT %s FROM jhi_date_time_wrapper where id=%d", fieldName, id); + } + + private void assertThatValueFromSqlRowSetIsEqualToExpectedValue(SqlRowSet sqlRowSet, String expectedValue) { + while (sqlRowSet.next()) { + String dbValue = sqlRowSet.getString(1); + + assertThat(dbValue).isNotNull(); + assertThat(dbValue).isEqualTo(expectedValue); + } + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/AssertUtils.java b/src/test/java/com/oguerreiro/resilient/domain/AssertUtils.java new file mode 100644 index 0000000..6f1a0f4 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/AssertUtils.java @@ -0,0 +1,15 @@ +package com.oguerreiro.resilient.domain; + +import java.math.BigDecimal; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Comparator; + +public class AssertUtils { + + public static Comparator zonedDataTimeSameInstant = Comparator.nullsFirst( + (e1, a2) -> e1.withZoneSameInstant(ZoneOffset.UTC).compareTo(a2.withZoneSameInstant(ZoneOffset.UTC)) + ); + + public static Comparator bigDecimalCompareTo = Comparator.nullsFirst((e1, a2) -> e1.compareTo(a2)); +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/AuthorityAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/AuthorityAsserts.java new file mode 100644 index 0000000..4beacf1 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/AuthorityAsserts.java @@ -0,0 +1,56 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AuthorityAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertAuthorityAllPropertiesEquals(Authority expected, Authority actual) { + assertAuthorityAutoGeneratedPropertiesEquals(expected, actual); + assertAuthorityAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertAuthorityAllUpdatablePropertiesEquals(Authority expected, Authority actual) { + assertAuthorityUpdatableFieldsEquals(expected, actual); + assertAuthorityUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertAuthorityAutoGeneratedPropertiesEquals(Authority expected, Authority actual) {} + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertAuthorityUpdatableFieldsEquals(Authority expected, Authority actual) { + assertThat(expected) + .as("Verify Authority relevant properties") + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertAuthorityUpdatableRelationshipsEquals(Authority expected, Authority actual) {} +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/AuthorityTest.java b/src/test/java/com/oguerreiro/resilient/domain/AuthorityTest.java new file mode 100644 index 0000000..bd82e5e --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/AuthorityTest.java @@ -0,0 +1,34 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.AuthorityTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class AuthorityTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(Authority.class); + Authority authority1 = getAuthoritySample1(); + Authority authority2 = new Authority(); + assertThat(authority1).isNotEqualTo(authority2); + + authority2.setName(authority1.getName()); + assertThat(authority1).isEqualTo(authority2); + + authority2 = getAuthoritySample2(); + assertThat(authority1).isNotEqualTo(authority2); + } + + @Test + void hashCodeVerifier() { + Authority authority = new Authority(); + assertThat(authority.hashCode()).isZero(); + + Authority authority1 = getAuthoritySample1(); + authority.setName(authority1.getName()); + assertThat(authority).hasSameHashCodeAs(authority1); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/AuthorityTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/AuthorityTestSamples.java new file mode 100644 index 0000000..66f31ca --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/AuthorityTestSamples.java @@ -0,0 +1,18 @@ +package com.oguerreiro.resilient.domain; + +import java.util.UUID; + +public class AuthorityTestSamples { + + public static Authority getAuthoritySample1() { + return new Authority().name("name1"); + } + + public static Authority getAuthoritySample2() { + return new Authority().name("name2"); + } + + public static Authority getAuthorityRandomSampleGenerator() { + return new Authority().name(UUID.randomUUID().toString()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataAsserts.java new file mode 100644 index 0000000..830c1c7 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataAsserts.java @@ -0,0 +1,89 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.AssertUtils.bigDecimalCompareTo; +import static org.assertj.core.api.Assertions.assertThat; + +public class InputDataAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataAllPropertiesEquals(InputData expected, InputData actual) { + assertInputDataAutoGeneratedPropertiesEquals(expected, actual); + assertInputDataAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) + * set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataAllUpdatablePropertiesEquals(InputData expected, InputData actual) { + assertInputDataUpdatableFieldsEquals(expected, actual); + assertInputDataUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties + * (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataAutoGeneratedPropertiesEquals(InputData expected, InputData actual) { + assertThat(expected).as("Verify InputData auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUpdatableFieldsEquals(InputData expected, InputData actual) { + assertThat(expected).as("Verify InputData relevant properties") + .satisfies(e -> assertThat(e.getSourceValue()).as("check sourceValue").usingComparator(bigDecimalCompareTo) + .isEqualTo(actual.getSourceValue())) + .satisfies(e -> assertThat(e.getVariableValue()).as("check variableValue").usingComparator(bigDecimalCompareTo) + .isEqualTo(actual.getVariableValue())) + .satisfies(e -> assertThat(e.getImputedValue()).as("check imputedValue").usingComparator(bigDecimalCompareTo) + .isEqualTo(actual.getImputedValue())) + .satisfies(e -> assertThat(e.getSourceType()).as("check sourceType").isEqualTo(actual.getSourceType())) + .satisfies(e -> assertThat(e.getDataDate()).as("check dataDate").isEqualTo(actual.getDataDate())) + .satisfies(e -> assertThat(e.getChangeDate()).as("check changeDate").isEqualTo(actual.getChangeDate())) + .satisfies( + e -> assertThat(e.getChangeUsername()).as("check changeUsername").isEqualTo(actual.getChangeUsername())) + .satisfies(e -> assertThat(e.getDataSource()).as("check dataSource").isEqualTo(actual.getDataSource())) + .satisfies(e -> assertThat(e.getDataUser()).as("check dataUser").isEqualTo(actual.getDataUser())) + .satisfies(e -> assertThat(e.getDataComments()).as("check dataComments").isEqualTo(actual.getDataComments())) + .satisfies(e -> assertThat(e.getCreationDate()).as("check creationDate").isEqualTo(actual.getCreationDate())) + .satisfies(e -> assertThat(e.getCreationUsername()).as("check creationUsername") + .isEqualTo(actual.getCreationUsername())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUpdatableRelationshipsEquals(InputData expected, InputData actual) { + assertThat(expected).as("Verify InputData relationships") + .satisfies(e -> assertThat(e.getVariable()).as("check variable").isEqualTo(actual.getVariable())) + .satisfies(e -> assertThat(e.getPeriod()).as("check period").isEqualTo(actual.getPeriod())) + .satisfies(e -> assertThat(e.getSourceUnit()).as("check sourceUnit").isEqualTo(actual.getSourceUnit())) + .satisfies(e -> assertThat(e.getUnit()).as("check unit").isEqualTo(actual.getUnit())) + .satisfies(e -> assertThat(e.getOwner()).as("check owner").isEqualTo(actual.getOwner())) + .satisfies( + e -> assertThat(e.getSourceInputData()).as("check sourceInputData").isEqualTo(actual.getSourceInputData())) + .satisfies(e -> assertThat(e.getSourceInputDataUpload()).as("check sourceInputDataUpload") + .isEqualTo(actual.getSourceInputDataUpload())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataTest.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataTest.java new file mode 100644 index 0000000..bcb4c5b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataTest.java @@ -0,0 +1,141 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.InputDataTestSamples.getInputDataRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.InputDataTestSamples.getInputDataSample1; +import static com.oguerreiro.resilient.domain.InputDataTestSamples.getInputDataSample2; +import static com.oguerreiro.resilient.domain.InputDataUploadTestSamples.getInputDataUploadRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.OrganizationTestSamples.getOrganizationRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.PeriodTestSamples.getPeriodRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.UnitTestSamples.getUnitRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.VariableTestSamples.getVariableRandomSampleGenerator; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.oguerreiro.resilient.web.rest.TestUtil; + +class InputDataTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(InputData.class); + InputData inputData1 = getInputDataSample1(); + InputData inputData2 = new InputData(); + assertThat(inputData1).isNotEqualTo(inputData2); + + inputData2.setId(inputData1.getId()); + assertThat(inputData1).isEqualTo(inputData2); + + inputData2 = getInputDataSample2(); + assertThat(inputData1).isNotEqualTo(inputData2); + } + + @Test + void inputDataTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + InputData inputDataBack = getInputDataRandomSampleGenerator(); + + inputData.addInputData(inputDataBack); + assertThat(inputData.getInputData()).containsOnly(inputDataBack); + assertThat(inputDataBack.getSourceInputData()).isEqualTo(inputData); + + inputData.removeInputData(inputDataBack); + assertThat(inputData.getInputData()).doesNotContain(inputDataBack); + assertThat(inputDataBack.getSourceInputData()).isNull(); + + inputData.inputData(new HashSet<>(Set.of(inputDataBack))); + assertThat(inputData.getInputData()).containsOnly(inputDataBack); + assertThat(inputDataBack.getSourceInputData()).isEqualTo(inputData); + + inputData.setInputData(new HashSet<>()); + assertThat(inputData.getInputData()).doesNotContain(inputDataBack); + assertThat(inputDataBack.getSourceInputData()).isNull(); + } + + @Test + void variableTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + Variable variableBack = getVariableRandomSampleGenerator(); + + inputData.setVariable(variableBack); + assertThat(inputData.getVariable()).isEqualTo(variableBack); + + inputData.variable(null); + assertThat(inputData.getVariable()).isNull(); + } + + @Test + void periodTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + Period periodBack = getPeriodRandomSampleGenerator(); + + inputData.setPeriod(periodBack); + assertThat(inputData.getPeriod()).isEqualTo(periodBack); + + inputData.period(null); + assertThat(inputData.getPeriod()).isNull(); + } + + @Test + void sourceUnitTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + inputData.setSourceUnit(unitBack); + assertThat(inputData.getSourceUnit()).isEqualTo(unitBack); + + inputData.sourceUnit(null); + assertThat(inputData.getSourceUnit()).isNull(); + } + + @Test + void unitTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + inputData.setUnit(unitBack); + assertThat(inputData.getUnit()).isEqualTo(unitBack); + + inputData.unit(null); + assertThat(inputData.getUnit()).isNull(); + } + + @Test + void ownerTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + Organization organizationBack = getOrganizationRandomSampleGenerator(); + + inputData.setOwner(organizationBack); + assertThat(inputData.getOwner()).isEqualTo(organizationBack); + + inputData.owner(null); + assertThat(inputData.getOwner()).isNull(); + } + + @Test + void sourceInputDataTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + InputData inputDataBack = getInputDataRandomSampleGenerator(); + + inputData.setSourceInputData(inputDataBack); + assertThat(inputData.getSourceInputData()).isEqualTo(inputDataBack); + + inputData.sourceInputData(null); + assertThat(inputData.getSourceInputData()).isNull(); + } + + @Test + void sourceInputDataUploadTest() { + InputData inputData = getInputDataRandomSampleGenerator(); + InputDataUpload inputDataUploadBack = getInputDataUploadRandomSampleGenerator(); + + inputData.setSourceInputDataUpload(inputDataUploadBack); + assertThat(inputData.getSourceInputDataUpload()).isEqualTo(inputDataUploadBack); + + inputData.sourceInputDataUpload(null); + assertThat(inputData.getSourceInputDataUpload()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataTestSamples.java new file mode 100644 index 0000000..f03b463 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataTestSamples.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class InputDataTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static InputData getInputDataSample1() { + return new InputData() + .id(1L) + .changeUsername("changeUsername1") + .dataSource("dataSource1") + .dataUser("dataUser1") + .dataComments("dataComments1") + .creationUsername("creationUsername1") + .version(1); + } + + public static InputData getInputDataSample2() { + return new InputData() + .id(2L) + .changeUsername("changeUsername2") + .dataSource("dataSource2") + .dataUser("dataUser2") + .dataComments("dataComments2") + .creationUsername("creationUsername2") + .version(2); + } + + public static InputData getInputDataRandomSampleGenerator() { + return new InputData() + .id(longCount.incrementAndGet()) + .changeUsername(UUID.randomUUID().toString()) + .dataSource(UUID.randomUUID().toString()) + .dataUser(UUID.randomUUID().toString()) + .dataComments(UUID.randomUUID().toString()) + .creationUsername(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadAsserts.java new file mode 100644 index 0000000..c2b1c64 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadAsserts.java @@ -0,0 +1,80 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InputDataUploadAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadAllPropertiesEquals(InputDataUpload expected, InputDataUpload actual) { + assertInputDataUploadAutoGeneratedPropertiesEquals(expected, actual); + assertInputDataUploadAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) + * set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadAllUpdatablePropertiesEquals(InputDataUpload expected, + InputDataUpload actual) { + assertInputDataUploadUpdatableFieldsEquals(expected, actual); + assertInputDataUploadUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties + * (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadAutoGeneratedPropertiesEquals(InputDataUpload expected, + InputDataUpload actual) { + assertThat(expected).as("Verify InputDataUpload auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadUpdatableFieldsEquals(InputDataUpload expected, InputDataUpload actual) { + assertThat(expected).as("Verify InputDataUpload relevant properties") + .satisfies(e -> assertThat(e.getTitle()).as("check title").isEqualTo(actual.getTitle())) + .satisfies(e -> assertThat(e.getDataFile()).as("check dataFile").isEqualTo(actual.getDataFile())) + .satisfies(e -> assertThat(e.getDataFileContentType()).as("check dataFile contenty type") + .isEqualTo(actual.getDataFileContentType())) + .satisfies( + e -> assertThat(e.getUploadFileName()).as("check uploadFileName").isEqualTo(actual.getUploadFileName())) + .satisfies(e -> assertThat(e.getDiskFileName()).as("check diskFileName").isEqualTo(actual.getDiskFileName())) + .satisfies(e -> assertThat(e.getType()).as("check type").isEqualTo(actual.getType())) + .satisfies(e -> assertThat(e.getState()).as("check state").isEqualTo(actual.getState())) + .satisfies(e -> assertThat(e.getComments()).as("check comments").isEqualTo(actual.getComments())) + .satisfies(e -> assertThat(e.getCreationDate()).as("check creationDate").isEqualTo(actual.getCreationDate())) + .satisfies(e -> assertThat(e.getCreationUsername()).as("check creationUsername") + .isEqualTo(actual.getCreationUsername())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadUpdatableRelationshipsEquals(InputDataUpload expected, + InputDataUpload actual) { + assertThat(expected).as("Verify InputDataUpload relationships") + .satisfies(e -> assertThat(e.getPeriod()).as("check period").isEqualTo(actual.getPeriod())) + .satisfies(e -> assertThat(e.getOwner()).as("check owner").isEqualTo(actual.getOwner())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogAsserts.java new file mode 100644 index 0000000..e8397df --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogAsserts.java @@ -0,0 +1,67 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InputDataUploadLogAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadLogAllPropertiesEquals(InputDataUploadLog expected, InputDataUploadLog actual) { + assertInputDataUploadLogAutoGeneratedPropertiesEquals(expected, actual); + assertInputDataUploadLogAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadLogAllUpdatablePropertiesEquals(InputDataUploadLog expected, InputDataUploadLog actual) { + assertInputDataUploadLogUpdatableFieldsEquals(expected, actual); + assertInputDataUploadLogUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadLogAutoGeneratedPropertiesEquals(InputDataUploadLog expected, InputDataUploadLog actual) { + assertThat(expected) + .as("Verify InputDataUploadLog auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadLogUpdatableFieldsEquals(InputDataUploadLog expected, InputDataUploadLog actual) { + assertThat(expected) + .as("Verify InputDataUploadLog relevant properties") + .satisfies(e -> assertThat(e.getLogMessage()).as("check logMessage").isEqualTo(actual.getLogMessage())) + .satisfies(e -> assertThat(e.getCreationDate()).as("check creationDate").isEqualTo(actual.getCreationDate())) + .satisfies(e -> assertThat(e.getCreationUsername()).as("check creationUsername").isEqualTo(actual.getCreationUsername())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertInputDataUploadLogUpdatableRelationshipsEquals(InputDataUploadLog expected, InputDataUploadLog actual) { + assertThat(expected) + .as("Verify InputDataUploadLog relationships") + .satisfies(e -> assertThat(e.getInputDataUpload()).as("check inputDataUpload").isEqualTo(actual.getInputDataUpload())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogTest.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogTest.java new file mode 100644 index 0000000..38963cd --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogTest.java @@ -0,0 +1,37 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.InputDataUploadLogTestSamples.*; +import static com.oguerreiro.resilient.domain.InputDataUploadTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class InputDataUploadLogTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(InputDataUploadLog.class); + InputDataUploadLog inputDataUploadLog1 = getInputDataUploadLogSample1(); + InputDataUploadLog inputDataUploadLog2 = new InputDataUploadLog(); + assertThat(inputDataUploadLog1).isNotEqualTo(inputDataUploadLog2); + + inputDataUploadLog2.setId(inputDataUploadLog1.getId()); + assertThat(inputDataUploadLog1).isEqualTo(inputDataUploadLog2); + + inputDataUploadLog2 = getInputDataUploadLogSample2(); + assertThat(inputDataUploadLog1).isNotEqualTo(inputDataUploadLog2); + } + + @Test + void inputDataUploadTest() { + InputDataUploadLog inputDataUploadLog = getInputDataUploadLogRandomSampleGenerator(); + InputDataUpload inputDataUploadBack = getInputDataUploadRandomSampleGenerator(); + + inputDataUploadLog.setInputDataUpload(inputDataUploadBack); + assertThat(inputDataUploadLog.getInputDataUpload()).isEqualTo(inputDataUploadBack); + + inputDataUploadLog.inputDataUpload(null); + assertThat(inputDataUploadLog.getInputDataUpload()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogTestSamples.java new file mode 100644 index 0000000..39c9a8a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadLogTestSamples.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class InputDataUploadLogTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static InputDataUploadLog getInputDataUploadLogSample1() { + return new InputDataUploadLog().id(1L).logMessage("logMessage1").creationUsername("creationUsername1").version(1); + } + + public static InputDataUploadLog getInputDataUploadLogSample2() { + return new InputDataUploadLog().id(2L).logMessage("logMessage2").creationUsername("creationUsername2").version(2); + } + + public static InputDataUploadLog getInputDataUploadLogRandomSampleGenerator() { + return new InputDataUploadLog() + .id(longCount.incrementAndGet()) + .logMessage(UUID.randomUUID().toString()) + .creationUsername(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadTest.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadTest.java new file mode 100644 index 0000000..c006574 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadTest.java @@ -0,0 +1,102 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.InputDataTestSamples.getInputDataRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.InputDataUploadLogTestSamples.getInputDataUploadLogRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.InputDataUploadTestSamples.getInputDataUploadRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.InputDataUploadTestSamples.getInputDataUploadSample1; +import static com.oguerreiro.resilient.domain.InputDataUploadTestSamples.getInputDataUploadSample2; +import static com.oguerreiro.resilient.domain.OrganizationTestSamples.getOrganizationRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.PeriodTestSamples.getPeriodRandomSampleGenerator; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.oguerreiro.resilient.web.rest.TestUtil; + +class InputDataUploadTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(InputDataUpload.class); + InputDataUpload inputDataUpload1 = getInputDataUploadSample1(); + InputDataUpload inputDataUpload2 = new InputDataUpload(); + assertThat(inputDataUpload1).isNotEqualTo(inputDataUpload2); + + inputDataUpload2.setId(inputDataUpload1.getId()); + assertThat(inputDataUpload1).isEqualTo(inputDataUpload2); + + inputDataUpload2 = getInputDataUploadSample2(); + assertThat(inputDataUpload1).isNotEqualTo(inputDataUpload2); + } + + @Test + void inputDataTest() { + InputDataUpload inputDataUpload = getInputDataUploadRandomSampleGenerator(); + InputData inputDataBack = getInputDataRandomSampleGenerator(); + + inputDataUpload.addInputData(inputDataBack); + assertThat(inputDataUpload.getInputData()).containsOnly(inputDataBack); + assertThat(inputDataBack.getSourceInputDataUpload()).isEqualTo(inputDataUpload); + + inputDataUpload.removeInputData(inputDataBack); + assertThat(inputDataUpload.getInputData()).doesNotContain(inputDataBack); + assertThat(inputDataBack.getSourceInputDataUpload()).isNull(); + + inputDataUpload.inputData(new HashSet<>(Set.of(inputDataBack))); + assertThat(inputDataUpload.getInputData()).containsOnly(inputDataBack); + assertThat(inputDataBack.getSourceInputDataUpload()).isEqualTo(inputDataUpload); + + inputDataUpload.setInputData(new HashSet<>()); + assertThat(inputDataUpload.getInputData()).doesNotContain(inputDataBack); + assertThat(inputDataBack.getSourceInputDataUpload()).isNull(); + } + + @Test + void inputDataUploadLogTest() { + InputDataUpload inputDataUpload = getInputDataUploadRandomSampleGenerator(); + InputDataUploadLog inputDataUploadLogBack = getInputDataUploadLogRandomSampleGenerator(); + + inputDataUpload.addInputDataUploadLog(inputDataUploadLogBack); + assertThat(inputDataUpload.getInputDataUploadLogs()).containsOnly(inputDataUploadLogBack); + assertThat(inputDataUploadLogBack.getInputDataUpload()).isEqualTo(inputDataUpload); + + inputDataUpload.removeInputDataUploadLog(inputDataUploadLogBack); + assertThat(inputDataUpload.getInputDataUploadLogs()).doesNotContain(inputDataUploadLogBack); + assertThat(inputDataUploadLogBack.getInputDataUpload()).isNull(); + + inputDataUpload.inputDataUploadLogs(new HashSet<>(Set.of(inputDataUploadLogBack))); + assertThat(inputDataUpload.getInputDataUploadLogs()).containsOnly(inputDataUploadLogBack); + assertThat(inputDataUploadLogBack.getInputDataUpload()).isEqualTo(inputDataUpload); + + inputDataUpload.setInputDataUploadLogs(new HashSet<>()); + assertThat(inputDataUpload.getInputDataUploadLogs()).doesNotContain(inputDataUploadLogBack); + assertThat(inputDataUploadLogBack.getInputDataUpload()).isNull(); + } + + @Test + void periodTest() { + InputDataUpload inputDataUpload = getInputDataUploadRandomSampleGenerator(); + Period periodBack = getPeriodRandomSampleGenerator(); + + inputDataUpload.setPeriod(periodBack); + assertThat(inputDataUpload.getPeriod()).isEqualTo(periodBack); + + inputDataUpload.period(null); + assertThat(inputDataUpload.getPeriod()).isNull(); + } + + @Test + void ownerTest() { + InputDataUpload inputDataUpload = getInputDataUploadRandomSampleGenerator(); + Organization organizationBack = getOrganizationRandomSampleGenerator(); + + inputDataUpload.setOwner(organizationBack); + assertThat(inputDataUpload.getOwner()).isEqualTo(organizationBack); + + inputDataUpload.owner(null); + assertThat(inputDataUpload.getOwner()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadTestSamples.java new file mode 100644 index 0000000..72b959d --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/InputDataUploadTestSamples.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class InputDataUploadTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static InputDataUpload getInputDataUploadSample1() { + return new InputDataUpload() + .id(1L) + .title("title1") + .uploadFileName("uploadFileName1") + .diskFileName("diskFileName1") + .comments("comments1") + .creationUsername("creationUsername1") + .version(1); + } + + public static InputDataUpload getInputDataUploadSample2() { + return new InputDataUpload() + .id(2L) + .title("title2") + .uploadFileName("uploadFileName2") + .diskFileName("diskFileName2") + .comments("comments2") + .creationUsername("creationUsername2") + .version(2); + } + + public static InputDataUpload getInputDataUploadRandomSampleGenerator() { + return new InputDataUpload() + .id(longCount.incrementAndGet()) + .title(UUID.randomUUID().toString()) + .uploadFileName(UUID.randomUUID().toString()) + .diskFileName(UUID.randomUUID().toString()) + .comments(UUID.randomUUID().toString()) + .creationUsername(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyAsserts.java new file mode 100644 index 0000000..1646679 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyAsserts.java @@ -0,0 +1,70 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MetadataPropertyAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataPropertyAllPropertiesEquals(MetadataProperty expected, MetadataProperty actual) { + assertMetadataPropertyAutoGeneratedPropertiesEquals(expected, actual); + assertMetadataPropertyAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataPropertyAllUpdatablePropertiesEquals(MetadataProperty expected, MetadataProperty actual) { + assertMetadataPropertyUpdatableFieldsEquals(expected, actual); + assertMetadataPropertyUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataPropertyAutoGeneratedPropertiesEquals(MetadataProperty expected, MetadataProperty actual) { + assertThat(expected) + .as("Verify MetadataProperty auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataPropertyUpdatableFieldsEquals(MetadataProperty expected, MetadataProperty actual) { + assertThat(expected) + .as("Verify MetadataProperty relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getMandatory()).as("check mandatory").isEqualTo(actual.getMandatory())) + .satisfies(e -> assertThat(e.getMetadataType()).as("check metadataType").isEqualTo(actual.getMetadataType())) + .satisfies(e -> assertThat(e.getPattern()).as("check pattern").isEqualTo(actual.getPattern())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataPropertyUpdatableRelationshipsEquals(MetadataProperty expected, MetadataProperty actual) { + assertThat(expected) + .as("Verify MetadataProperty relationships") + .satisfies(e -> assertThat(e.getOrganizationTypes()).as("check organizationTypes").isEqualTo(actual.getOrganizationTypes())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyTest.java b/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyTest.java new file mode 100644 index 0000000..63dd9b8 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyTest.java @@ -0,0 +1,49 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.MetadataPropertyTestSamples.*; +import static com.oguerreiro.resilient.domain.OrganizationTypeTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class MetadataPropertyTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(MetadataProperty.class); + MetadataProperty metadataProperty1 = getMetadataPropertySample1(); + MetadataProperty metadataProperty2 = new MetadataProperty(); + assertThat(metadataProperty1).isNotEqualTo(metadataProperty2); + + metadataProperty2.setId(metadataProperty1.getId()); + assertThat(metadataProperty1).isEqualTo(metadataProperty2); + + metadataProperty2 = getMetadataPropertySample2(); + assertThat(metadataProperty1).isNotEqualTo(metadataProperty2); + } + + @Test + void organizationTypeTest() { + MetadataProperty metadataProperty = getMetadataPropertyRandomSampleGenerator(); + OrganizationType organizationTypeBack = getOrganizationTypeRandomSampleGenerator(); + + metadataProperty.addOrganizationType(organizationTypeBack); + assertThat(metadataProperty.getOrganizationTypes()).containsOnly(organizationTypeBack); + assertThat(organizationTypeBack.getMetadataProperties()).containsOnly(metadataProperty); + + metadataProperty.removeOrganizationType(organizationTypeBack); + assertThat(metadataProperty.getOrganizationTypes()).doesNotContain(organizationTypeBack); + assertThat(organizationTypeBack.getMetadataProperties()).doesNotContain(metadataProperty); + + metadataProperty.organizationTypes(new HashSet<>(Set.of(organizationTypeBack))); + assertThat(metadataProperty.getOrganizationTypes()).containsOnly(organizationTypeBack); + assertThat(organizationTypeBack.getMetadataProperties()).containsOnly(metadataProperty); + + metadataProperty.setOrganizationTypes(new HashSet<>()); + assertThat(metadataProperty.getOrganizationTypes()).doesNotContain(organizationTypeBack); + assertThat(organizationTypeBack.getMetadataProperties()).doesNotContain(metadataProperty); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyTestSamples.java new file mode 100644 index 0000000..1fe6cde --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/MetadataPropertyTestSamples.java @@ -0,0 +1,31 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class MetadataPropertyTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static MetadataProperty getMetadataPropertySample1() { + return new MetadataProperty().id(1L).code("code1").name("name1").description("description1").pattern("pattern1").version(1); + } + + public static MetadataProperty getMetadataPropertySample2() { + return new MetadataProperty().id(2L).code("code2").name("name2").description("description2").pattern("pattern2").version(2); + } + + public static MetadataProperty getMetadataPropertyRandomSampleGenerator() { + return new MetadataProperty() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .pattern(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/MetadataValueAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/MetadataValueAsserts.java new file mode 100644 index 0000000..fb44f13 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/MetadataValueAsserts.java @@ -0,0 +1,66 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MetadataValueAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataValueAllPropertiesEquals(MetadataValue expected, MetadataValue actual) { + assertMetadataValueAutoGeneratedPropertiesEquals(expected, actual); + assertMetadataValueAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataValueAllUpdatablePropertiesEquals(MetadataValue expected, MetadataValue actual) { + assertMetadataValueUpdatableFieldsEquals(expected, actual); + assertMetadataValueUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataValueAutoGeneratedPropertiesEquals(MetadataValue expected, MetadataValue actual) { + assertThat(expected) + .as("Verify MetadataValue auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataValueUpdatableFieldsEquals(MetadataValue expected, MetadataValue actual) { + assertThat(expected) + .as("Verify MetadataValue relevant properties") + .satisfies( + e -> assertThat(e.getMetadataPropertyCode()).as("check metadataPropertyCode").isEqualTo(actual.getMetadataPropertyCode()) + ) + .satisfies(e -> assertThat(e.getTargetDomainKey()).as("check targetDomainKey").isEqualTo(actual.getTargetDomainKey())) + .satisfies(e -> assertThat(e.getTargetDomainId()).as("check targetDomainId").isEqualTo(actual.getTargetDomainId())) + .satisfies(e -> assertThat(e.getValue()).as("check value").isEqualTo(actual.getValue())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertMetadataValueUpdatableRelationshipsEquals(MetadataValue expected, MetadataValue actual) {} +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/MetadataValueTest.java b/src/test/java/com/oguerreiro/resilient/domain/MetadataValueTest.java new file mode 100644 index 0000000..a75c779 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/MetadataValueTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.MetadataValueTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class MetadataValueTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(MetadataValue.class); + MetadataValue metadataValue1 = getMetadataValueSample1(); + MetadataValue metadataValue2 = new MetadataValue(); + assertThat(metadataValue1).isNotEqualTo(metadataValue2); + + metadataValue2.setId(metadataValue1.getId()); + assertThat(metadataValue1).isEqualTo(metadataValue2); + + metadataValue2 = getMetadataValueSample2(); + assertThat(metadataValue1).isNotEqualTo(metadataValue2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/MetadataValueTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/MetadataValueTestSamples.java new file mode 100644 index 0000000..1d6dd44 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/MetadataValueTestSamples.java @@ -0,0 +1,43 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class MetadataValueTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static MetadataValue getMetadataValueSample1() { + return new MetadataValue() + .id(1L) + .metadataPropertyCode("metadataPropertyCode1") + .targetDomainKey("targetDomainKey1") + .targetDomainId(1L) + .value("value1") + .version(1); + } + + public static MetadataValue getMetadataValueSample2() { + return new MetadataValue() + .id(2L) + .metadataPropertyCode("metadataPropertyCode2") + .targetDomainKey("targetDomainKey2") + .targetDomainId(2L) + .value("value2") + .version(2); + } + + public static MetadataValue getMetadataValueRandomSampleGenerator() { + return new MetadataValue() + .id(longCount.incrementAndGet()) + .metadataPropertyCode(UUID.randomUUID().toString()) + .targetDomainKey(UUID.randomUUID().toString()) + .targetDomainId(longCount.incrementAndGet()) + .value(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/MvelTest.java b/src/test/java/com/oguerreiro/resilient/domain/MvelTest.java new file mode 100644 index 0000000..ae0621a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/MvelTest.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.domain; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mvel2.MVEL; + +public class MvelTest { + @Test + void mvelEvalTest() throws Exception { + String expression = "int i = 10; " + "int ii = 20; " + "i*ii+10"; + expression = "10.10**2"; + //expression = "i = Math.pow(10.10, 2); Math.round(i * 100) / 100.0"; + //expression = "Math.round(Math.pow(10.10, 2) * 100) / 100.0"; + + Map variables = new HashMap(); + + System.out.println(evaluateExpression(expression, variables)); + } + + public Object evaluateExpression(String expression, Map variables) { + // Compiling the expression for performance (optional) + Serializable compiledExpression = MVEL.compileExpression(expression); + // Execute the compiled expression with variables + return MVEL.executeExpression(compiledExpression, variables); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OrganizationAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/OrganizationAsserts.java new file mode 100644 index 0000000..7c475ab --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OrganizationAsserts.java @@ -0,0 +1,71 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OrganizationAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationAllPropertiesEquals(Organization expected, Organization actual) { + assertOrganizationAutoGeneratedPropertiesEquals(expected, actual); + assertOrganizationAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationAllUpdatablePropertiesEquals(Organization expected, Organization actual) { + assertOrganizationUpdatableFieldsEquals(expected, actual); + assertOrganizationUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationAutoGeneratedPropertiesEquals(Organization expected, Organization actual) { + assertThat(expected) + .as("Verify Organization auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationUpdatableFieldsEquals(Organization expected, Organization actual) { + assertThat(expected) + .as("Verify Organization relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getImage()).as("check image").isEqualTo(actual.getImage())) + .satisfies(e -> assertThat(e.getImageContentType()).as("check image contenty type").isEqualTo(actual.getImageContentType())) + .satisfies(e -> assertThat(e.getInputInventory()).as("check inputInventory").isEqualTo(actual.getInputInventory())) + .satisfies(e -> assertThat(e.getOutputInventory()).as("check outputInventory").isEqualTo(actual.getOutputInventory())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationUpdatableRelationshipsEquals(Organization expected, Organization actual) { + assertThat(expected) + .as("Verify Organization relationships") + .satisfies(e -> assertThat(e.getParent()).as("check parent").isEqualTo(actual.getParent())) + .satisfies(e -> assertThat(e.getOrganizationType()).as("check organizationType").isEqualTo(actual.getOrganizationType())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OrganizationTest.java b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTest.java new file mode 100644 index 0000000..0662359 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTest.java @@ -0,0 +1,77 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.OrganizationTestSamples.getOrganizationRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.OrganizationTestSamples.getOrganizationSample1; +import static com.oguerreiro.resilient.domain.OrganizationTestSamples.getOrganizationSample2; +import static com.oguerreiro.resilient.domain.OrganizationTypeTestSamples.getOrganizationTypeRandomSampleGenerator; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.oguerreiro.resilient.web.rest.TestUtil; + +class OrganizationTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(Organization.class); + Organization organization1 = getOrganizationSample1(); + Organization organization2 = new Organization(); + assertThat(organization1).isNotEqualTo(organization2); + + organization2.setId(organization1.getId()); + assertThat(organization1).isEqualTo(organization2); + + organization2 = getOrganizationSample2(); + assertThat(organization1).isNotEqualTo(organization2); + } + + @Test + void childTest() { + Organization organization = getOrganizationRandomSampleGenerator(); + Organization organizationBack = getOrganizationRandomSampleGenerator(); + + organization.addChild(organizationBack); + assertThat(organization.getChildren()).containsOnly(organizationBack); + assertThat(organizationBack.getParent()).isEqualTo(organization); + + organization.removeChild(organizationBack); + assertThat(organization.getChildren()).doesNotContain(organizationBack); + assertThat(organizationBack.getParent()).isNull(); + + organization.children(new ArrayList<>(Set.of(organizationBack))); + assertThat(organization.getChildren()).containsOnly(organizationBack); + assertThat(organizationBack.getParent()).isEqualTo(organization); + + organization.setChildren(new ArrayList<>()); + assertThat(organization.getChildren()).doesNotContain(organizationBack); + assertThat(organizationBack.getParent()).isNull(); + } + + @Test + void parentTest() { + Organization organization = getOrganizationRandomSampleGenerator(); + Organization organizationBack = getOrganizationRandomSampleGenerator(); + + organization.setParent(organizationBack); + assertThat(organization.getParent()).isEqualTo(organizationBack); + + organization.parent(null); + assertThat(organization.getParent()).isNull(); + } + + @Test + void organizationTypeTest() { + Organization organization = getOrganizationRandomSampleGenerator(); + OrganizationType organizationTypeBack = getOrganizationTypeRandomSampleGenerator(); + + organization.setOrganizationType(organizationTypeBack); + assertThat(organization.getOrganizationType()).isEqualTo(organizationTypeBack); + + organization.organizationType(null); + assertThat(organization.getOrganizationType()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OrganizationTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTestSamples.java new file mode 100644 index 0000000..21b6c97 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTestSamples.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class OrganizationTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static Organization getOrganizationSample1() { + return new Organization().id(1L).code("code1").name("name1").version(1); + } + + public static Organization getOrganizationSample2() { + return new Organization().id(2L).code("code2").name("name2").version(2); + } + + public static Organization getOrganizationRandomSampleGenerator() { + return new Organization() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeAsserts.java new file mode 100644 index 0000000..9e06880 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeAsserts.java @@ -0,0 +1,70 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OrganizationTypeAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationTypeAllPropertiesEquals(OrganizationType expected, OrganizationType actual) { + assertOrganizationTypeAutoGeneratedPropertiesEquals(expected, actual); + assertOrganizationTypeAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationTypeAllUpdatablePropertiesEquals(OrganizationType expected, OrganizationType actual) { + assertOrganizationTypeUpdatableFieldsEquals(expected, actual); + assertOrganizationTypeUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationTypeAutoGeneratedPropertiesEquals(OrganizationType expected, OrganizationType actual) { + assertThat(expected) + .as("Verify OrganizationType auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationTypeUpdatableFieldsEquals(OrganizationType expected, OrganizationType actual) { + assertThat(expected) + .as("Verify OrganizationType relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getNature()).as("check nature").isEqualTo(actual.getNature())) + .satisfies(e -> assertThat(e.getIcon()).as("check icon").isEqualTo(actual.getIcon())) + .satisfies(e -> assertThat(e.getIconContentType()).as("check icon contenty type").isEqualTo(actual.getIconContentType())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOrganizationTypeUpdatableRelationshipsEquals(OrganizationType expected, OrganizationType actual) { + assertThat(expected) + .as("Verify OrganizationType relationships") + .satisfies(e -> assertThat(e.getMetadataProperties()).as("check metadataProperties").isEqualTo(actual.getMetadataProperties())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeTest.java b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeTest.java new file mode 100644 index 0000000..64893dc --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeTest.java @@ -0,0 +1,68 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.MetadataPropertyTestSamples.*; +import static com.oguerreiro.resilient.domain.OrganizationTestSamples.*; +import static com.oguerreiro.resilient.domain.OrganizationTypeTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class OrganizationTypeTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(OrganizationType.class); + OrganizationType organizationType1 = getOrganizationTypeSample1(); + OrganizationType organizationType2 = new OrganizationType(); + assertThat(organizationType1).isNotEqualTo(organizationType2); + + organizationType2.setId(organizationType1.getId()); + assertThat(organizationType1).isEqualTo(organizationType2); + + organizationType2 = getOrganizationTypeSample2(); + assertThat(organizationType1).isNotEqualTo(organizationType2); + } + + @Test + void organizationTest() { + OrganizationType organizationType = getOrganizationTypeRandomSampleGenerator(); + Organization organizationBack = getOrganizationRandomSampleGenerator(); + + organizationType.addOrganization(organizationBack); + assertThat(organizationType.getOrganizations()).containsOnly(organizationBack); + assertThat(organizationBack.getOrganizationType()).isEqualTo(organizationType); + + organizationType.removeOrganization(organizationBack); + assertThat(organizationType.getOrganizations()).doesNotContain(organizationBack); + assertThat(organizationBack.getOrganizationType()).isNull(); + + organizationType.organizations(new HashSet<>(Set.of(organizationBack))); + assertThat(organizationType.getOrganizations()).containsOnly(organizationBack); + assertThat(organizationBack.getOrganizationType()).isEqualTo(organizationType); + + organizationType.setOrganizations(new HashSet<>()); + assertThat(organizationType.getOrganizations()).doesNotContain(organizationBack); + assertThat(organizationBack.getOrganizationType()).isNull(); + } + + @Test + void metadataPropertiesTest() { + OrganizationType organizationType = getOrganizationTypeRandomSampleGenerator(); + MetadataProperty metadataPropertyBack = getMetadataPropertyRandomSampleGenerator(); + + organizationType.addMetadataProperties(metadataPropertyBack); + assertThat(organizationType.getMetadataProperties()).containsOnly(metadataPropertyBack); + + organizationType.removeMetadataProperties(metadataPropertyBack); + assertThat(organizationType.getMetadataProperties()).doesNotContain(metadataPropertyBack); + + organizationType.metadataProperties(new HashSet<>(Set.of(metadataPropertyBack))); + assertThat(organizationType.getMetadataProperties()).containsOnly(metadataPropertyBack); + + organizationType.setMetadataProperties(new HashSet<>()); + assertThat(organizationType.getMetadataProperties()).doesNotContain(metadataPropertyBack); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeTestSamples.java new file mode 100644 index 0000000..e04d583 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OrganizationTypeTestSamples.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class OrganizationTypeTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static OrganizationType getOrganizationTypeSample1() { + return new OrganizationType().id(1L).code("code1").name("name1").description("description1").version(1); + } + + public static OrganizationType getOrganizationTypeSample2() { + return new OrganizationType().id(2L).code("code2").name("name2").description("description2").version(2); + } + + public static OrganizationType getOrganizationTypeRandomSampleGenerator() { + return new OrganizationType() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OutputDataAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/OutputDataAsserts.java new file mode 100644 index 0000000..cce8714 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OutputDataAsserts.java @@ -0,0 +1,69 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.AssertUtils.bigDecimalCompareTo; +import static org.assertj.core.api.Assertions.assertThat; + +public class OutputDataAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOutputDataAllPropertiesEquals(OutputData expected, OutputData actual) { + assertOutputDataAutoGeneratedPropertiesEquals(expected, actual); + assertOutputDataAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOutputDataAllUpdatablePropertiesEquals(OutputData expected, OutputData actual) { + assertOutputDataUpdatableFieldsEquals(expected, actual); + assertOutputDataUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOutputDataAutoGeneratedPropertiesEquals(OutputData expected, OutputData actual) { + assertThat(expected) + .as("Verify OutputData auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOutputDataUpdatableFieldsEquals(OutputData expected, OutputData actual) { + assertThat(expected) + .as("Verify OutputData relevant properties") + .satisfies(e -> assertThat(e.getValue()).as("check value").usingComparator(bigDecimalCompareTo).isEqualTo(actual.getValue())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertOutputDataUpdatableRelationshipsEquals(OutputData expected, OutputData actual) { + assertThat(expected) + .as("Verify OutputData relationships") + .satisfies(e -> assertThat(e.getVariable()).as("check variable").isEqualTo(actual.getVariable())) + .satisfies(e -> assertThat(e.getPeriod()).as("check period").isEqualTo(actual.getPeriod())) + .satisfies(e -> assertThat(e.getPeriodVersion()).as("check periodVersion").isEqualTo(actual.getPeriodVersion())) + .satisfies(e -> assertThat(e.getBaseUnit()).as("check baseUnit").isEqualTo(actual.getBaseUnit())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OutputDataTest.java b/src/test/java/com/oguerreiro/resilient/domain/OutputDataTest.java new file mode 100644 index 0000000..1db7d6b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OutputDataTest.java @@ -0,0 +1,76 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.OutputDataTestSamples.*; +import static com.oguerreiro.resilient.domain.PeriodTestSamples.*; +import static com.oguerreiro.resilient.domain.PeriodVersionTestSamples.*; +import static com.oguerreiro.resilient.domain.UnitTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class OutputDataTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(OutputData.class); + OutputData outputData1 = getOutputDataSample1(); + OutputData outputData2 = new OutputData(); + assertThat(outputData1).isNotEqualTo(outputData2); + + outputData2.setId(outputData1.getId()); + assertThat(outputData1).isEqualTo(outputData2); + + outputData2 = getOutputDataSample2(); + assertThat(outputData1).isNotEqualTo(outputData2); + } + + @Test + void variableTest() { + OutputData outputData = getOutputDataRandomSampleGenerator(); + Variable variableBack = getVariableRandomSampleGenerator(); + + outputData.setVariable(variableBack); + assertThat(outputData.getVariable()).isEqualTo(variableBack); + + outputData.variable(null); + assertThat(outputData.getVariable()).isNull(); + } + + @Test + void periodTest() { + OutputData outputData = getOutputDataRandomSampleGenerator(); + Period periodBack = getPeriodRandomSampleGenerator(); + + outputData.setPeriod(periodBack); + assertThat(outputData.getPeriod()).isEqualTo(periodBack); + + outputData.period(null); + assertThat(outputData.getPeriod()).isNull(); + } + + @Test + void periodVersionTest() { + OutputData outputData = getOutputDataRandomSampleGenerator(); + PeriodVersion periodVersionBack = getPeriodVersionRandomSampleGenerator(); + + outputData.setPeriodVersion(periodVersionBack); + assertThat(outputData.getPeriodVersion()).isEqualTo(periodVersionBack); + + outputData.periodVersion(null); + assertThat(outputData.getPeriodVersion()).isNull(); + } + + @Test + void baseUnitTest() { + OutputData outputData = getOutputDataRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + outputData.setBaseUnit(unitBack); + assertThat(outputData.getBaseUnit()).isEqualTo(unitBack); + + outputData.baseUnit(null); + assertThat(outputData.getBaseUnit()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/OutputDataTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/OutputDataTestSamples.java new file mode 100644 index 0000000..a3c5f9e --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/OutputDataTestSamples.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class OutputDataTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static OutputData getOutputDataSample1() { + return new OutputData().id(1L).version(1); + } + + public static OutputData getOutputDataSample2() { + return new OutputData().id(2L).version(2); + } + + public static OutputData getOutputDataRandomSampleGenerator() { + return new OutputData().id(longCount.incrementAndGet()).version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/PeriodAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/PeriodAsserts.java new file mode 100644 index 0000000..4e70e52 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/PeriodAsserts.java @@ -0,0 +1,67 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PeriodAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodAllPropertiesEquals(Period expected, Period actual) { + assertPeriodAutoGeneratedPropertiesEquals(expected, actual); + assertPeriodAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodAllUpdatablePropertiesEquals(Period expected, Period actual) { + assertPeriodUpdatableFieldsEquals(expected, actual); + assertPeriodUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodAutoGeneratedPropertiesEquals(Period expected, Period actual) { + assertThat(expected) + .as("Verify Period auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodUpdatableFieldsEquals(Period expected, Period actual) { + assertThat(expected) + .as("Verify Period relevant properties") + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getBeginDate()).as("check beginDate").isEqualTo(actual.getBeginDate())) + .satisfies(e -> assertThat(e.getEndDate()).as("check endDate").isEqualTo(actual.getEndDate())) + .satisfies(e -> assertThat(e.getState()).as("check state").isEqualTo(actual.getState())) + .satisfies(e -> assertThat(e.getCreationDate()).as("check creationDate").isEqualTo(actual.getCreationDate())) + .satisfies(e -> assertThat(e.getCreationUsername()).as("check creationUsername").isEqualTo(actual.getCreationUsername())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodUpdatableRelationshipsEquals(Period expected, Period actual) {} +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/PeriodTest.java b/src/test/java/com/oguerreiro/resilient/domain/PeriodTest.java new file mode 100644 index 0000000..9516c5d --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/PeriodTest.java @@ -0,0 +1,54 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.PeriodTestSamples.getPeriodRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.PeriodTestSamples.getPeriodSample1; +import static com.oguerreiro.resilient.domain.PeriodTestSamples.getPeriodSample2; +import static com.oguerreiro.resilient.domain.PeriodVersionTestSamples.getPeriodVersionRandomSampleGenerator; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.oguerreiro.resilient.web.rest.TestUtil; + +class PeriodTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(Period.class); + Period period1 = getPeriodSample1(); + Period period2 = new Period(); + assertThat(period1).isNotEqualTo(period2); + + period2.setId(period1.getId()); + assertThat(period1).isEqualTo(period2); + + period2 = getPeriodSample2(); + assertThat(period1).isNotEqualTo(period2); + } + + @Test + void periodVersionTest() { + Period period = getPeriodRandomSampleGenerator(); + PeriodVersion periodVersionBack = getPeriodVersionRandomSampleGenerator(); + + period.addPeriodVersion(periodVersionBack); + assertThat(period.getPeriodVersions()).containsOnly(periodVersionBack); + assertThat(periodVersionBack.getPeriod()).isEqualTo(period); + + period.removePeriodVersion(periodVersionBack); + assertThat(period.getPeriodVersions()).doesNotContain(periodVersionBack); + assertThat(periodVersionBack.getPeriod()).isNull(); + + period.periodVersions(new ArrayList<>(List.of(periodVersionBack))); + assertThat(period.getPeriodVersions()).containsOnly(periodVersionBack); + assertThat(periodVersionBack.getPeriod()).isEqualTo(period); + + period.setPeriodVersions(new ArrayList<>()); + assertThat(period.getPeriodVersions()).doesNotContain(periodVersionBack); + assertThat(periodVersionBack.getPeriod()).isNull(); + } + +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/PeriodTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/PeriodTestSamples.java new file mode 100644 index 0000000..7e26ecb --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/PeriodTestSamples.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class PeriodTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static Period getPeriodSample1() { + return new Period().id(1L).name("name1").description("description1").creationUsername("creationUsername1").version(1); + } + + public static Period getPeriodSample2() { + return new Period().id(2L).name("name2").description("description2").creationUsername("creationUsername2").version(2); + } + + public static Period getPeriodRandomSampleGenerator() { + return new Period() + .id(longCount.incrementAndGet()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .creationUsername(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionAsserts.java new file mode 100644 index 0000000..ea73f73 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionAsserts.java @@ -0,0 +1,70 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PeriodVersionAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodVersionAllPropertiesEquals(PeriodVersion expected, PeriodVersion actual) { + assertPeriodVersionAutoGeneratedPropertiesEquals(expected, actual); + assertPeriodVersionAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodVersionAllUpdatablePropertiesEquals(PeriodVersion expected, PeriodVersion actual) { + assertPeriodVersionUpdatableFieldsEquals(expected, actual); + assertPeriodVersionUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodVersionAutoGeneratedPropertiesEquals(PeriodVersion expected, PeriodVersion actual) { + assertThat(expected) + .as("Verify PeriodVersion auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodVersionUpdatableFieldsEquals(PeriodVersion expected, PeriodVersion actual) { + assertThat(expected) + .as("Verify PeriodVersion relevant properties") + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getPeriodVersion()).as("check periodVersion").isEqualTo(actual.getPeriodVersion())) + .satisfies(e -> assertThat(e.getState()).as("check state").isEqualTo(actual.getState())) + .satisfies(e -> assertThat(e.getCreationDate()).as("check creationDate").isEqualTo(actual.getCreationDate())) + .satisfies(e -> assertThat(e.getCreationUsername()).as("check creationUsername").isEqualTo(actual.getCreationUsername())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertPeriodVersionUpdatableRelationshipsEquals(PeriodVersion expected, PeriodVersion actual) { + assertThat(expected) + .as("Verify PeriodVersion relationships") + .satisfies(e -> assertThat(e.getPeriod()).as("check period").isEqualTo(actual.getPeriod())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionTest.java b/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionTest.java new file mode 100644 index 0000000..dc73237 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionTest.java @@ -0,0 +1,40 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.PeriodTestSamples.getPeriodRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.PeriodVersionTestSamples.getPeriodVersionRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.PeriodVersionTestSamples.getPeriodVersionSample1; +import static com.oguerreiro.resilient.domain.PeriodVersionTestSamples.getPeriodVersionSample2; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.oguerreiro.resilient.web.rest.TestUtil; + +class PeriodVersionTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(PeriodVersion.class); + PeriodVersion periodVersion1 = getPeriodVersionSample1(); + PeriodVersion periodVersion2 = new PeriodVersion(); + assertThat(periodVersion1).isNotEqualTo(periodVersion2); + + periodVersion2.setId(periodVersion1.getId()); + assertThat(periodVersion1).isEqualTo(periodVersion2); + + periodVersion2 = getPeriodVersionSample2(); + assertThat(periodVersion1).isNotEqualTo(periodVersion2); + } + + @Test + void periodTest() { + PeriodVersion periodVersion = getPeriodVersionRandomSampleGenerator(); + Period periodBack = getPeriodRandomSampleGenerator(); + + periodVersion.setPeriod(periodBack); + assertThat(periodVersion.getPeriod()).isEqualTo(periodBack); + + periodVersion.period(null); + assertThat(periodVersion.getPeriod()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionTestSamples.java new file mode 100644 index 0000000..7c0befe --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/PeriodVersionTestSamples.java @@ -0,0 +1,43 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class PeriodVersionTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static PeriodVersion getPeriodVersionSample1() { + return new PeriodVersion() + .id(1L) + .name("name1") + .description("description1") + .periodVersion(1) + .creationUsername("creationUsername1") + .version(1); + } + + public static PeriodVersion getPeriodVersionSample2() { + return new PeriodVersion() + .id(2L) + .name("name2") + .description("description2") + .periodVersion(2) + .creationUsername("creationUsername2") + .version(2); + } + + public static PeriodVersion getPeriodVersionRandomSampleGenerator() { + return new PeriodVersion() + .id(longCount.incrementAndGet()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .periodVersion(intCount.incrementAndGet()) + .creationUsername(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/UnitAsserts.java new file mode 100644 index 0000000..fce3a77 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitAsserts.java @@ -0,0 +1,76 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.AssertUtils.bigDecimalCompareTo; +import static org.assertj.core.api.Assertions.assertThat; + +public class UnitAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitAllPropertiesEquals(Unit expected, Unit actual) { + assertUnitAutoGeneratedPropertiesEquals(expected, actual); + assertUnitAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitAllUpdatablePropertiesEquals(Unit expected, Unit actual) { + assertUnitUpdatableFieldsEquals(expected, actual); + assertUnitUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitAutoGeneratedPropertiesEquals(Unit expected, Unit actual) { + assertThat(expected) + .as("Verify Unit auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitUpdatableFieldsEquals(Unit expected, Unit actual) { + assertThat(expected) + .as("Verify Unit relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getSymbol()).as("check symbol").isEqualTo(actual.getSymbol())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies( + e -> + assertThat(e.getConvertionRate()) + .as("check convertionRate") + .usingComparator(bigDecimalCompareTo) + .isEqualTo(actual.getConvertionRate()) + ) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitUpdatableRelationshipsEquals(Unit expected, Unit actual) { + assertThat(expected) + .as("Verify Unit relationships") + .satisfies(e -> assertThat(e.getUnitType()).as("check unitType").isEqualTo(actual.getUnitType())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitConverterAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/UnitConverterAsserts.java new file mode 100644 index 0000000..f6a211b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitConverterAsserts.java @@ -0,0 +1,68 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UnitConverterAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitConverterAllPropertiesEquals(UnitConverter expected, UnitConverter actual) { + assertUnitConverterAutoGeneratedPropertiesEquals(expected, actual); + assertUnitConverterAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitConverterAllUpdatablePropertiesEquals(UnitConverter expected, UnitConverter actual) { + assertUnitConverterUpdatableFieldsEquals(expected, actual); + assertUnitConverterUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitConverterAutoGeneratedPropertiesEquals(UnitConverter expected, UnitConverter actual) { + assertThat(expected) + .as("Verify UnitConverter auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitConverterUpdatableFieldsEquals(UnitConverter expected, UnitConverter actual) { + assertThat(expected) + .as("Verify UnitConverter relevant properties") + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getConvertionFormula()).as("check convertionFormula").isEqualTo(actual.getConvertionFormula())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitConverterUpdatableRelationshipsEquals(UnitConverter expected, UnitConverter actual) { + assertThat(expected) + .as("Verify UnitConverter relationships") + .satisfies(e -> assertThat(e.getFromUnit()).as("check fromUnit").isEqualTo(actual.getFromUnit())) + .satisfies(e -> assertThat(e.getToUnit()).as("check toUnit").isEqualTo(actual.getToUnit())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitConverterTest.java b/src/test/java/com/oguerreiro/resilient/domain/UnitConverterTest.java new file mode 100644 index 0000000..4605a0a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitConverterTest.java @@ -0,0 +1,49 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.UnitConverterTestSamples.*; +import static com.oguerreiro.resilient.domain.UnitTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class UnitConverterTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(UnitConverter.class); + UnitConverter unitConverter1 = getUnitConverterSample1(); + UnitConverter unitConverter2 = new UnitConverter(); + assertThat(unitConverter1).isNotEqualTo(unitConverter2); + + unitConverter2.setId(unitConverter1.getId()); + assertThat(unitConverter1).isEqualTo(unitConverter2); + + unitConverter2 = getUnitConverterSample2(); + assertThat(unitConverter1).isNotEqualTo(unitConverter2); + } + + @Test + void fromUnitTest() { + UnitConverter unitConverter = getUnitConverterRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + unitConverter.setFromUnit(unitBack); + assertThat(unitConverter.getFromUnit()).isEqualTo(unitBack); + + unitConverter.fromUnit(null); + assertThat(unitConverter.getFromUnit()).isNull(); + } + + @Test + void toUnitTest() { + UnitConverter unitConverter = getUnitConverterRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + unitConverter.setToUnit(unitBack); + assertThat(unitConverter.getToUnit()).isEqualTo(unitBack); + + unitConverter.toUnit(null); + assertThat(unitConverter.getToUnit()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitConverterTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/UnitConverterTestSamples.java new file mode 100644 index 0000000..5cf1958 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitConverterTestSamples.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class UnitConverterTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static UnitConverter getUnitConverterSample1() { + return new UnitConverter().id(1L).name("name1").description("description1").convertionFormula("convertionFormula1").version(1); + } + + public static UnitConverter getUnitConverterSample2() { + return new UnitConverter().id(2L).name("name2").description("description2").convertionFormula("convertionFormula2").version(2); + } + + public static UnitConverter getUnitConverterRandomSampleGenerator() { + return new UnitConverter() + .id(longCount.incrementAndGet()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .convertionFormula(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitTest.java b/src/test/java/com/oguerreiro/resilient/domain/UnitTest.java new file mode 100644 index 0000000..662a04d --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitTest.java @@ -0,0 +1,40 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.UnitTestSamples.getUnitRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.UnitTestSamples.getUnitSample1; +import static com.oguerreiro.resilient.domain.UnitTestSamples.getUnitSample2; +import static com.oguerreiro.resilient.domain.UnitTypeTestSamples.getUnitTypeRandomSampleGenerator; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.oguerreiro.resilient.web.rest.TestUtil; + +class UnitTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(Unit.class); + Unit unit1 = getUnitSample1(); + Unit unit2 = new Unit(); + assertThat(unit1).isNotEqualTo(unit2); + + unit2.setId(unit1.getId()); + assertThat(unit1).isEqualTo(unit2); + + unit2 = getUnitSample2(); + assertThat(unit1).isNotEqualTo(unit2); + } + + @Test + void unitTypeTest() { + Unit unit = getUnitRandomSampleGenerator(); + UnitType unitTypeBack = getUnitTypeRandomSampleGenerator(); + + unit.setUnitType(unitTypeBack); + assertThat(unit.getUnitType()).isEqualTo(unitTypeBack); + + unit.unitType(null); + assertThat(unit.getUnitType()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/UnitTestSamples.java new file mode 100644 index 0000000..b51b436 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitTestSamples.java @@ -0,0 +1,31 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class UnitTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static Unit getUnitSample1() { + return new Unit().id(1L).code("code1").symbol("symbol1").name("name1").description("description1").version(1); + } + + public static Unit getUnitSample2() { + return new Unit().id(2L).code("code2").symbol("symbol2").name("name2").description("description2").version(2); + } + + public static Unit getUnitRandomSampleGenerator() { + return new Unit() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .symbol(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitTypeAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/UnitTypeAsserts.java new file mode 100644 index 0000000..e7a62cc --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitTypeAsserts.java @@ -0,0 +1,63 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UnitTypeAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitTypeAllPropertiesEquals(UnitType expected, UnitType actual) { + assertUnitTypeAutoGeneratedPropertiesEquals(expected, actual); + assertUnitTypeAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitTypeAllUpdatablePropertiesEquals(UnitType expected, UnitType actual) { + assertUnitTypeUpdatableFieldsEquals(expected, actual); + assertUnitTypeUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitTypeAutoGeneratedPropertiesEquals(UnitType expected, UnitType actual) { + assertThat(expected) + .as("Verify UnitType auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitTypeUpdatableFieldsEquals(UnitType expected, UnitType actual) { + assertThat(expected) + .as("Verify UnitType relevant properties") + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getValueType()).as("check valueType").isEqualTo(actual.getValueType())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertUnitTypeUpdatableRelationshipsEquals(UnitType expected, UnitType actual) {} +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitTypeTest.java b/src/test/java/com/oguerreiro/resilient/domain/UnitTypeTest.java new file mode 100644 index 0000000..9163e2c --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitTypeTest.java @@ -0,0 +1,49 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.UnitTestSamples.*; +import static com.oguerreiro.resilient.domain.UnitTypeTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class UnitTypeTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(UnitType.class); + UnitType unitType1 = getUnitTypeSample1(); + UnitType unitType2 = new UnitType(); + assertThat(unitType1).isNotEqualTo(unitType2); + + unitType2.setId(unitType1.getId()); + assertThat(unitType1).isEqualTo(unitType2); + + unitType2 = getUnitTypeSample2(); + assertThat(unitType1).isNotEqualTo(unitType2); + } + + @Test + void unitTest() { + UnitType unitType = getUnitTypeRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + unitType.addUnit(unitBack); + assertThat(unitType.getUnits()).containsOnly(unitBack); + assertThat(unitBack.getUnitType()).isEqualTo(unitType); + + unitType.removeUnit(unitBack); + assertThat(unitType.getUnits()).doesNotContain(unitBack); + assertThat(unitBack.getUnitType()).isNull(); + + unitType.units(new HashSet<>(Set.of(unitBack))); + assertThat(unitType.getUnits()).containsOnly(unitBack); + assertThat(unitBack.getUnitType()).isEqualTo(unitType); + + unitType.setUnits(new HashSet<>()); + assertThat(unitType.getUnits()).doesNotContain(unitBack); + assertThat(unitBack.getUnitType()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/UnitTypeTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/UnitTypeTestSamples.java new file mode 100644 index 0000000..1a1e697 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/UnitTypeTestSamples.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class UnitTypeTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static UnitType getUnitTypeSample1() { + return new UnitType().id(1L).name("name1").description("description1").version(1); + } + + public static UnitType getUnitTypeSample2() { + return new UnitType().id(2L).name("name2").description("description2").version(2); + } + + public static UnitType getUnitTypeRandomSampleGenerator() { + return new UnitType() + .id(longCount.incrementAndGet()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/VariableAsserts.java new file mode 100644 index 0000000..0039b32 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableAsserts.java @@ -0,0 +1,74 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class VariableAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableAllPropertiesEquals(Variable expected, Variable actual) { + assertVariableAutoGeneratedPropertiesEquals(expected, actual); + assertVariableAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableAllUpdatablePropertiesEquals(Variable expected, Variable actual) { + assertVariableUpdatableFieldsEquals(expected, actual); + assertVariableUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableAutoGeneratedPropertiesEquals(Variable expected, Variable actual) { + assertThat(expected) + .as("Verify Variable auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableUpdatableFieldsEquals(Variable expected, Variable actual) { + assertThat(expected) + .as("Verify Variable relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getVariableIndex()).as("check variableIndex").isEqualTo(actual.getVariableIndex())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getInput()).as("check input").isEqualTo(actual.getInput())) + .satisfies(e -> assertThat(e.getInputMode()).as("check inputMode").isEqualTo(actual.getInputMode())) + .satisfies(e -> assertThat(e.getOutput()).as("check output").isEqualTo(actual.getOutput())) + .satisfies(e -> assertThat(e.getOutputFormula()).as("check outputFormula").isEqualTo(actual.getOutputFormula())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableUpdatableRelationshipsEquals(Variable expected, Variable actual) { + assertThat(expected) + .as("Verify Variable relationships") + .satisfies(e -> assertThat(e.getBaseUnit()).as("check baseUnit").isEqualTo(actual.getBaseUnit())) + .satisfies(e -> assertThat(e.getVariableScope()).as("check variableScope").isEqualTo(actual.getVariableScope())) + .satisfies(e -> assertThat(e.getVariableCategory()).as("check variableCategory").isEqualTo(actual.getVariableCategory())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryAsserts.java new file mode 100644 index 0000000..6ef6446 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryAsserts.java @@ -0,0 +1,67 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class VariableCategoryAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableCategoryAllPropertiesEquals(VariableCategory expected, VariableCategory actual) { + assertVariableCategoryAutoGeneratedPropertiesEquals(expected, actual); + assertVariableCategoryAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableCategoryAllUpdatablePropertiesEquals(VariableCategory expected, VariableCategory actual) { + assertVariableCategoryUpdatableFieldsEquals(expected, actual); + assertVariableCategoryUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableCategoryAutoGeneratedPropertiesEquals(VariableCategory expected, VariableCategory actual) { + assertThat(expected) + .as("Verify VariableCategory auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableCategoryUpdatableFieldsEquals(VariableCategory expected, VariableCategory actual) { + assertThat(expected) + .as("Verify VariableCategory relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableCategoryUpdatableRelationshipsEquals(VariableCategory expected, VariableCategory actual) { + assertThat(expected) + .as("Verify VariableCategory relationships") + .satisfies(e -> assertThat(e.getVariableScope()).as("check variableScope").isEqualTo(actual.getVariableScope())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryTest.java b/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryTest.java new file mode 100644 index 0000000..7b4f91f --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryTest.java @@ -0,0 +1,62 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.VariableCategoryTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableScopeTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class VariableCategoryTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableCategory.class); + VariableCategory variableCategory1 = getVariableCategorySample1(); + VariableCategory variableCategory2 = new VariableCategory(); + assertThat(variableCategory1).isNotEqualTo(variableCategory2); + + variableCategory2.setId(variableCategory1.getId()); + assertThat(variableCategory1).isEqualTo(variableCategory2); + + variableCategory2 = getVariableCategorySample2(); + assertThat(variableCategory1).isNotEqualTo(variableCategory2); + } + + @Test + void variableTest() { + VariableCategory variableCategory = getVariableCategoryRandomSampleGenerator(); + Variable variableBack = getVariableRandomSampleGenerator(); + + variableCategory.addVariable(variableBack); + assertThat(variableCategory.getVariables()).containsOnly(variableBack); + assertThat(variableBack.getVariableCategory()).isEqualTo(variableCategory); + + variableCategory.removeVariable(variableBack); + assertThat(variableCategory.getVariables()).doesNotContain(variableBack); + assertThat(variableBack.getVariableCategory()).isNull(); + + variableCategory.variables(new HashSet<>(Set.of(variableBack))); + assertThat(variableCategory.getVariables()).containsOnly(variableBack); + assertThat(variableBack.getVariableCategory()).isEqualTo(variableCategory); + + variableCategory.setVariables(new HashSet<>()); + assertThat(variableCategory.getVariables()).doesNotContain(variableBack); + assertThat(variableBack.getVariableCategory()).isNull(); + } + + @Test + void variableScopeTest() { + VariableCategory variableCategory = getVariableCategoryRandomSampleGenerator(); + VariableScope variableScopeBack = getVariableScopeRandomSampleGenerator(); + + variableCategory.setVariableScope(variableScopeBack); + assertThat(variableCategory.getVariableScope()).isEqualTo(variableScopeBack); + + variableCategory.variableScope(null); + assertThat(variableCategory.getVariableScope()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryTestSamples.java new file mode 100644 index 0000000..038c86d --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableCategoryTestSamples.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class VariableCategoryTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static VariableCategory getVariableCategorySample1() { + return new VariableCategory().id(1L).code("code1").name("name1").description("description1").version(1); + } + + public static VariableCategory getVariableCategorySample2() { + return new VariableCategory().id(2L).code("code2").name("name2").description("description2").version(2); + } + + public static VariableCategory getVariableCategoryRandomSampleGenerator() { + return new VariableCategory() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableClassAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/VariableClassAsserts.java new file mode 100644 index 0000000..bdf7b9a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableClassAsserts.java @@ -0,0 +1,66 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class VariableClassAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableClassAllPropertiesEquals(VariableClass expected, VariableClass actual) { + assertVariableClassAutoGeneratedPropertiesEquals(expected, actual); + assertVariableClassAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableClassAllUpdatablePropertiesEquals(VariableClass expected, VariableClass actual) { + assertVariableClassUpdatableFieldsEquals(expected, actual); + assertVariableClassUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableClassAutoGeneratedPropertiesEquals(VariableClass expected, VariableClass actual) { + assertThat(expected) + .as("Verify VariableClass auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableClassUpdatableFieldsEquals(VariableClass expected, VariableClass actual) { + assertThat(expected) + .as("Verify VariableClass relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableClassUpdatableRelationshipsEquals(VariableClass expected, VariableClass actual) { + assertThat(expected) + .as("Verify VariableClass relationships") + .satisfies(e -> assertThat(e.getVariableClassType()).as("check variable class type").isEqualTo(actual.getVariableClassType())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableClassTest.java b/src/test/java/com/oguerreiro/resilient/domain/VariableClassTest.java new file mode 100644 index 0000000..533ac84 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableClassTest.java @@ -0,0 +1,37 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.VariableClassTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableClassTypeTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class VariableClassTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableClass.class); + VariableClass variableClass1 = getVariableClassSample1(); + VariableClass variableClass2 = new VariableClass(); + assertThat(variableClass1).isNotEqualTo(variableClass2); + + variableClass2.setId(variableClass1.getId()); + assertThat(variableClass1).isEqualTo(variableClass2); + + variableClass2 = getVariableClassSample2(); + assertThat(variableClass1).isNotEqualTo(variableClass2); + } + + @Test + void variableTest() { + VariableClass variableClass = getVariableClassRandomSampleGenerator(); + VariableClassType variableClassTypeBack = getVariableClassTypeRandomSampleGenerator(); + + variableClass.setVariableClassType(variableClassTypeBack); + assertThat(variableClass.getVariableClassType()).isEqualTo(variableClassTypeBack); + + variableClass.variableClassType(null); + assertThat(variableClass.getVariableClassType()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableClassTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/VariableClassTestSamples.java new file mode 100644 index 0000000..71753c8 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableClassTestSamples.java @@ -0,0 +1,29 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class VariableClassTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static VariableClass getVariableClassSample1() { + return new VariableClass().id(1L).code("code1").name("name1").version(1); + } + + public static VariableClass getVariableClassSample2() { + return new VariableClass().id(2L).code("code2").name("name2").version(2); + } + + public static VariableClass getVariableClassRandomSampleGenerator() { + return new VariableClass() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableClassTypeTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/VariableClassTypeTestSamples.java new file mode 100644 index 0000000..569a1d7 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableClassTypeTestSamples.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class VariableClassTypeTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static VariableClassType getVariableClassTypeSample1() { + return new VariableClassType().id(1L).code("code1").name("name1").label("label1").version(1); + } + + public static VariableClassType getVariableClassTypeSample2() { + return new VariableClassType().id(2L).code("code2").name("name2").label("label2").version(2); + } + + public static VariableClassType getVariableClassTypeRandomSampleGenerator() { + return new VariableClassType() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .label(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableScopeAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/VariableScopeAsserts.java new file mode 100644 index 0000000..77edb91 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableScopeAsserts.java @@ -0,0 +1,63 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class VariableScopeAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableScopeAllPropertiesEquals(VariableScope expected, VariableScope actual) { + assertVariableScopeAutoGeneratedPropertiesEquals(expected, actual); + assertVariableScopeAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableScopeAllUpdatablePropertiesEquals(VariableScope expected, VariableScope actual) { + assertVariableScopeUpdatableFieldsEquals(expected, actual); + assertVariableScopeUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableScopeAutoGeneratedPropertiesEquals(VariableScope expected, VariableScope actual) { + assertThat(expected) + .as("Verify VariableScope auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableScopeUpdatableFieldsEquals(VariableScope expected, VariableScope actual) { + assertThat(expected) + .as("Verify VariableScope relevant properties") + .satisfies(e -> assertThat(e.getCode()).as("check code").isEqualTo(actual.getCode())) + .satisfies(e -> assertThat(e.getName()).as("check name").isEqualTo(actual.getName())) + .satisfies(e -> assertThat(e.getDescription()).as("check description").isEqualTo(actual.getDescription())) + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableScopeUpdatableRelationshipsEquals(VariableScope expected, VariableScope actual) {} +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableScopeTest.java b/src/test/java/com/oguerreiro/resilient/domain/VariableScopeTest.java new file mode 100644 index 0000000..28f929d --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableScopeTest.java @@ -0,0 +1,72 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.VariableCategoryTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableScopeTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class VariableScopeTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableScope.class); + VariableScope variableScope1 = getVariableScopeSample1(); + VariableScope variableScope2 = new VariableScope(); + assertThat(variableScope1).isNotEqualTo(variableScope2); + + variableScope2.setId(variableScope1.getId()); + assertThat(variableScope1).isEqualTo(variableScope2); + + variableScope2 = getVariableScopeSample2(); + assertThat(variableScope1).isNotEqualTo(variableScope2); + } + + @Test + void variableCategoryTest() { + VariableScope variableScope = getVariableScopeRandomSampleGenerator(); + VariableCategory variableCategoryBack = getVariableCategoryRandomSampleGenerator(); + + variableScope.addVariableCategory(variableCategoryBack); + assertThat(variableScope.getVariableCategories()).containsOnly(variableCategoryBack); + assertThat(variableCategoryBack.getVariableScope()).isEqualTo(variableScope); + + variableScope.removeVariableCategory(variableCategoryBack); + assertThat(variableScope.getVariableCategories()).doesNotContain(variableCategoryBack); + assertThat(variableCategoryBack.getVariableScope()).isNull(); + + variableScope.variableCategories(new HashSet<>(Set.of(variableCategoryBack))); + assertThat(variableScope.getVariableCategories()).containsOnly(variableCategoryBack); + assertThat(variableCategoryBack.getVariableScope()).isEqualTo(variableScope); + + variableScope.setVariableCategories(new HashSet<>()); + assertThat(variableScope.getVariableCategories()).doesNotContain(variableCategoryBack); + assertThat(variableCategoryBack.getVariableScope()).isNull(); + } + + @Test + void variableTest() { + VariableScope variableScope = getVariableScopeRandomSampleGenerator(); + Variable variableBack = getVariableRandomSampleGenerator(); + + variableScope.addVariable(variableBack); + assertThat(variableScope.getVariables()).containsOnly(variableBack); + assertThat(variableBack.getVariableScope()).isEqualTo(variableScope); + + variableScope.removeVariable(variableBack); + assertThat(variableScope.getVariables()).doesNotContain(variableBack); + assertThat(variableBack.getVariableScope()).isNull(); + + variableScope.variables(new HashSet<>(Set.of(variableBack))); + assertThat(variableScope.getVariables()).containsOnly(variableBack); + assertThat(variableBack.getVariableScope()).isEqualTo(variableScope); + + variableScope.setVariables(new HashSet<>()); + assertThat(variableScope.getVariables()).doesNotContain(variableBack); + assertThat(variableBack.getVariableScope()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableScopeTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/VariableScopeTestSamples.java new file mode 100644 index 0000000..9aa07af --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableScopeTestSamples.java @@ -0,0 +1,30 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class VariableScopeTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static VariableScope getVariableScopeSample1() { + return new VariableScope().id(1L).code("code1").name("name1").description("description1").version(1); + } + + public static VariableScope getVariableScopeSample2() { + return new VariableScope().id(2L).code("code2").name("name2").description("description2").version(2); + } + + public static VariableScope getVariableScopeRandomSampleGenerator() { + return new VariableScope() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableTest.java b/src/test/java/com/oguerreiro/resilient/domain/VariableTest.java new file mode 100644 index 0000000..9f43f2c --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableTest.java @@ -0,0 +1,95 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.InputDataTestSamples.getInputDataRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.OutputDataTestSamples.getOutputDataRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.UnitTestSamples.getUnitRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.VariableCategoryTestSamples.getVariableCategoryRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.VariableClassTestSamples.getVariableClassRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.VariableScopeTestSamples.getVariableScopeRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.VariableTestSamples.getVariableRandomSampleGenerator; +import static com.oguerreiro.resilient.domain.VariableTestSamples.getVariableSample1; +import static com.oguerreiro.resilient.domain.VariableTestSamples.getVariableSample2; +import static com.oguerreiro.resilient.domain.VariableUnitsTestSamples.getVariableUnitsRandomSampleGenerator; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.oguerreiro.resilient.web.rest.TestUtil; + +class VariableTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(Variable.class); + Variable variable1 = getVariableSample1(); + Variable variable2 = new Variable(); + assertThat(variable1).isNotEqualTo(variable2); + + variable2.setId(variable1.getId()); + assertThat(variable1).isEqualTo(variable2); + + variable2 = getVariableSample2(); + assertThat(variable1).isNotEqualTo(variable2); + } + + @Test + void variableUnitsTest() { + Variable variable = getVariableRandomSampleGenerator(); + VariableUnits variableUnitsBack = getVariableUnitsRandomSampleGenerator(); + + variable.addVariableUnits(variableUnitsBack); + assertThat(variable.getVariableUnits()).containsOnly(variableUnitsBack); + assertThat(variableUnitsBack.getVariable()).isEqualTo(variable); + + variable.removeVariableUnits(variableUnitsBack); + assertThat(variable.getVariableUnits()).doesNotContain(variableUnitsBack); + assertThat(variableUnitsBack.getVariable()).isNull(); + + variable.variableUnits(new HashSet<>(Set.of(variableUnitsBack))); + assertThat(variable.getVariableUnits()).containsOnly(variableUnitsBack); + assertThat(variableUnitsBack.getVariable()).isEqualTo(variable); + + variable.setVariableUnits(new HashSet<>()); + assertThat(variable.getVariableUnits()).doesNotContain(variableUnitsBack); + assertThat(variableUnitsBack.getVariable()).isNull(); + } + + @Test + void baseUnitTest() { + Variable variable = getVariableRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + variable.setBaseUnit(unitBack); + assertThat(variable.getBaseUnit()).isEqualTo(unitBack); + + variable.baseUnit(null); + assertThat(variable.getBaseUnit()).isNull(); + } + + @Test + void variableScopeTest() { + Variable variable = getVariableRandomSampleGenerator(); + VariableScope variableScopeBack = getVariableScopeRandomSampleGenerator(); + + variable.setVariableScope(variableScopeBack); + assertThat(variable.getVariableScope()).isEqualTo(variableScopeBack); + + variable.variableScope(null); + assertThat(variable.getVariableScope()).isNull(); + } + + @Test + void variableCategoryTest() { + Variable variable = getVariableRandomSampleGenerator(); + VariableCategory variableCategoryBack = getVariableCategoryRandomSampleGenerator(); + + variable.setVariableCategory(variableCategoryBack); + assertThat(variable.getVariableCategory()).isEqualTo(variableCategoryBack); + + variable.variableCategory(null); + assertThat(variable.getVariableCategory()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/VariableTestSamples.java new file mode 100644 index 0000000..af43c42 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableTestSamples.java @@ -0,0 +1,46 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class VariableTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static Variable getVariableSample1() { + return new Variable() + .id(1L) + .code("code1") + .variableIndex(1) + .name("name1") + .description("description1") + .outputFormula("outputFormula1") + .version(1); + } + + public static Variable getVariableSample2() { + return new Variable() + .id(2L) + .code("code2") + .variableIndex(2) + .name("name2") + .description("description2") + .outputFormula("outputFormula2") + .version(2); + } + + public static Variable getVariableRandomSampleGenerator() { + return new Variable() + .id(longCount.incrementAndGet()) + .code(UUID.randomUUID().toString()) + .variableIndex(intCount.incrementAndGet()) + .name(UUID.randomUUID().toString()) + .description(UUID.randomUUID().toString()) + .outputFormula(UUID.randomUUID().toString()) + .version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsAsserts.java b/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsAsserts.java new file mode 100644 index 0000000..5a075d7 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsAsserts.java @@ -0,0 +1,65 @@ +package com.oguerreiro.resilient.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +public class VariableUnitsAsserts { + + /** + * Asserts that the entity has all properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableUnitsAllPropertiesEquals(VariableUnits expected, VariableUnits actual) { + assertVariableUnitsAutoGeneratedPropertiesEquals(expected, actual); + assertVariableUnitsAllUpdatablePropertiesEquals(expected, actual); + } + + /** + * Asserts that the entity has all updatable properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableUnitsAllUpdatablePropertiesEquals(VariableUnits expected, VariableUnits actual) { + assertVariableUnitsUpdatableFieldsEquals(expected, actual); + assertVariableUnitsUpdatableRelationshipsEquals(expected, actual); + } + + /** + * Asserts that the entity has all the auto generated properties (fields/relationships) set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableUnitsAutoGeneratedPropertiesEquals(VariableUnits expected, VariableUnits actual) { + assertThat(expected) + .as("Verify VariableUnits auto generated properties") + .satisfies(e -> assertThat(e.getId()).as("check id").isEqualTo(actual.getId())); + } + + /** + * Asserts that the entity has all the updatable fields set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableUnitsUpdatableFieldsEquals(VariableUnits expected, VariableUnits actual) { + assertThat(expected) + .as("Verify VariableUnits relevant properties") + .satisfies(e -> assertThat(e.getVersion()).as("check version").isEqualTo(actual.getVersion())); + } + + /** + * Asserts that the entity has all the updatable relationships set. + * + * @param expected the expected entity + * @param actual the actual entity + */ + public static void assertVariableUnitsUpdatableRelationshipsEquals(VariableUnits expected, VariableUnits actual) { + assertThat(expected) + .as("Verify VariableUnits relationships") + .satisfies(e -> assertThat(e.getVariable()).as("check variable").isEqualTo(actual.getVariable())) + .satisfies(e -> assertThat(e.getUnit()).as("check unit").isEqualTo(actual.getUnit())); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsTest.java b/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsTest.java new file mode 100644 index 0000000..bd803df --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsTest.java @@ -0,0 +1,50 @@ +package com.oguerreiro.resilient.domain; + +import static com.oguerreiro.resilient.domain.UnitTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableTestSamples.*; +import static com.oguerreiro.resilient.domain.VariableUnitsTestSamples.*; +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class VariableUnitsTest { + + @Test + void equalsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableUnits.class); + VariableUnits variableUnits1 = getVariableUnitsSample1(); + VariableUnits variableUnits2 = new VariableUnits(); + assertThat(variableUnits1).isNotEqualTo(variableUnits2); + + variableUnits2.setId(variableUnits1.getId()); + assertThat(variableUnits1).isEqualTo(variableUnits2); + + variableUnits2 = getVariableUnitsSample2(); + assertThat(variableUnits1).isNotEqualTo(variableUnits2); + } + + @Test + void variableTest() { + VariableUnits variableUnits = getVariableUnitsRandomSampleGenerator(); + Variable variableBack = getVariableRandomSampleGenerator(); + + variableUnits.setVariable(variableBack); + assertThat(variableUnits.getVariable()).isEqualTo(variableBack); + + variableUnits.variable(null); + assertThat(variableUnits.getVariable()).isNull(); + } + + @Test + void unitTest() { + VariableUnits variableUnits = getVariableUnitsRandomSampleGenerator(); + Unit unitBack = getUnitRandomSampleGenerator(); + + variableUnits.setUnit(unitBack); + assertThat(variableUnits.getUnit()).isEqualTo(unitBack); + + variableUnits.unit(null); + assertThat(variableUnits.getUnit()).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsTestSamples.java b/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsTestSamples.java new file mode 100644 index 0000000..6dc4b9c --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/domain/VariableUnitsTestSamples.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.domain; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class VariableUnitsTestSamples { + + private static final Random random = new Random(); + private static final AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + private static final AtomicInteger intCount = new AtomicInteger(random.nextInt() + (2 * Short.MAX_VALUE)); + + public static VariableUnits getVariableUnitsSample1() { + return new VariableUnits().id(1L).version(1); + } + + public static VariableUnits getVariableUnitsSample2() { + return new VariableUnits().id(2L).version(2); + } + + public static VariableUnits getVariableUnitsRandomSampleGenerator() { + return new VariableUnits().id(longCount.incrementAndGet()).version(intCount.incrementAndGet()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/repository/timezone/DateTimeWrapper.java b/src/test/java/com/oguerreiro/resilient/repository/timezone/DateTimeWrapper.java new file mode 100644 index 0000000..f52e48b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/repository/timezone/DateTimeWrapper.java @@ -0,0 +1,132 @@ +package com.oguerreiro.resilient.repository.timezone; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.time.*; +import java.util.Objects; + +@Entity +@Table(name = "jhi_date_time_wrapper") +public class DateTimeWrapper implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "instant") + private Instant instant; + + @Column(name = "local_date_time") + private LocalDateTime localDateTime; + + @Column(name = "offset_date_time") + private OffsetDateTime offsetDateTime; + + @Column(name = "zoned_date_time") + private ZonedDateTime zonedDateTime; + + @Column(name = "local_time") + private LocalTime localTime; + + @Column(name = "offset_time") + private OffsetTime offsetTime; + + @Column(name = "local_date") + private LocalDate localDate; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Instant getInstant() { + return instant; + } + + public void setInstant(Instant instant) { + this.instant = instant; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public ZonedDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(ZonedDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public void setLocalTime(LocalTime localTime) { + this.localTime = localTime; + } + + public OffsetTime getOffsetTime() { + return offsetTime; + } + + public void setOffsetTime(OffsetTime offsetTime) { + this.offsetTime = offsetTime; + } + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DateTimeWrapper dateTimeWrapper = (DateTimeWrapper) o; + return !(dateTimeWrapper.getId() == null || getId() == null) && Objects.equals(getId(), dateTimeWrapper.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } + + // prettier-ignore + @Override + public String toString() { + return "TimeZoneTest{" + + "id=" + id + + ", instant=" + instant + + ", localDateTime=" + localDateTime + + ", offsetDateTime=" + offsetDateTime + + ", zonedDateTime=" + zonedDateTime + + '}'; + } +} diff --git a/src/test/java/com/oguerreiro/resilient/repository/timezone/DateTimeWrapperRepository.java b/src/test/java/com/oguerreiro/resilient/repository/timezone/DateTimeWrapperRepository.java new file mode 100644 index 0000000..3220f73 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/repository/timezone/DateTimeWrapperRepository.java @@ -0,0 +1,10 @@ +package com.oguerreiro.resilient.repository.timezone; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Spring Data JPA repository for the {@link DateTimeWrapper} entity. + */ +@Repository +public interface DateTimeWrapperRepository extends JpaRepository {} diff --git a/src/test/java/com/oguerreiro/resilient/security/DomainUserDetailsServiceIT.java b/src/test/java/com/oguerreiro/resilient/security/DomainUserDetailsServiceIT.java new file mode 100644 index 0000000..956625b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/security/DomainUserDetailsServiceIT.java @@ -0,0 +1,136 @@ +package com.oguerreiro.resilient.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.service.UserService; +import java.util.Locale; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integrations tests for {@link DomainUserDetailsService}. + */ +@Transactional +@IntegrationTest +class DomainUserDetailsServiceIT { + + private static final String USER_ONE_LOGIN = "test-user-one"; + private static final String USER_ONE_EMAIL = "test-user-one@localhost"; + private static final String USER_TWO_LOGIN = "test-user-two"; + private static final String USER_TWO_EMAIL = "test-user-two@localhost"; + private static final String USER_THREE_LOGIN = "test-user-three"; + private static final String USER_THREE_EMAIL = "test-user-three@localhost"; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + @Qualifier("userDetailsService") + private UserDetailsService domainUserDetailsService; + + public User getUserOne() { + User userOne = new User(); + userOne.setLogin(USER_ONE_LOGIN); + userOne.setPassword(RandomStringUtils.randomAlphanumeric(60)); + userOne.setActivated(true); + userOne.setEmail(USER_ONE_EMAIL); + userOne.setFirstName("userOne"); + userOne.setLastName("doe"); + userOne.setLangKey("en"); + return userOne; + } + + public User getUserTwo() { + User userTwo = new User(); + userTwo.setLogin(USER_TWO_LOGIN); + userTwo.setPassword(RandomStringUtils.randomAlphanumeric(60)); + userTwo.setActivated(true); + userTwo.setEmail(USER_TWO_EMAIL); + userTwo.setFirstName("userTwo"); + userTwo.setLastName("doe"); + userTwo.setLangKey("en"); + return userTwo; + } + + public User getUserThree() { + User userThree = new User(); + userThree.setLogin(USER_THREE_LOGIN); + userThree.setPassword(RandomStringUtils.randomAlphanumeric(60)); + userThree.setActivated(false); + userThree.setEmail(USER_THREE_EMAIL); + userThree.setFirstName("userThree"); + userThree.setLastName("doe"); + userThree.setLangKey("en"); + return userThree; + } + + @BeforeEach + public void init() { + userRepository.save(getUserOne()); + userRepository.save(getUserTwo()); + userRepository.save(getUserThree()); + } + + @AfterEach + public void cleanup() { + userService.deleteUser(USER_ONE_LOGIN); + userService.deleteUser(USER_TWO_LOGIN); + userService.deleteUser(USER_THREE_LOGIN); + } + + @Test + void assertThatUserCanBeFoundByLogin() { + UserDetails userDetails = domainUserDetailsService.loadUserByUsername(USER_ONE_LOGIN); + assertThat(userDetails).isNotNull(); + assertThat(userDetails.getUsername()).isEqualTo(USER_ONE_LOGIN); + } + + @Test + void assertThatUserCanBeFoundByLoginIgnoreCase() { + UserDetails userDetails = domainUserDetailsService.loadUserByUsername(USER_ONE_LOGIN.toUpperCase(Locale.ENGLISH)); + assertThat(userDetails).isNotNull(); + assertThat(userDetails.getUsername()).isEqualTo(USER_ONE_LOGIN); + } + + @Test + void assertThatUserCanBeFoundByEmail() { + UserDetails userDetails = domainUserDetailsService.loadUserByUsername(USER_TWO_EMAIL); + assertThat(userDetails).isNotNull(); + assertThat(userDetails.getUsername()).isEqualTo(USER_TWO_LOGIN); + } + + @Test + void assertThatUserCanBeFoundByEmailIgnoreCase() { + UserDetails userDetails = domainUserDetailsService.loadUserByUsername(USER_TWO_EMAIL.toUpperCase(Locale.ENGLISH)); + assertThat(userDetails).isNotNull(); + assertThat(userDetails.getUsername()).isEqualTo(USER_TWO_LOGIN); + } + + @Test + void assertThatEmailIsPrioritizedOverLogin() { + UserDetails userDetails = domainUserDetailsService.loadUserByUsername(USER_ONE_EMAIL); + assertThat(userDetails).isNotNull(); + assertThat(userDetails.getUsername()).isEqualTo(USER_ONE_LOGIN); + } + + @Test + void assertThatUserNotActivatedExceptionIsThrownForNotActivatedUsers() { + assertThatExceptionOfType(UserNotActivatedException.class).isThrownBy( + () -> domainUserDetailsService.loadUserByUsername(USER_THREE_LOGIN) + ); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/security/SecurityUtilsUnitTest.java b/src/test/java/com/oguerreiro/resilient/security/SecurityUtilsUnitTest.java new file mode 100644 index 0000000..e15fc36 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/security/SecurityUtilsUnitTest.java @@ -0,0 +1,92 @@ +package com.oguerreiro.resilient.security; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Test class for the {@link SecurityUtils} utility class. + */ +class SecurityUtilsUnitTest { + + @BeforeEach + @AfterEach + void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + void testGetCurrentUserLogin() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "admin")); + SecurityContextHolder.setContext(securityContext); + Optional login = SecurityUtils.getCurrentUserLogin(); + assertThat(login).contains("admin"); + } + + @Test + void testIsAuthenticated() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "admin")); + SecurityContextHolder.setContext(securityContext); + boolean isAuthenticated = SecurityUtils.isAuthenticated(); + assertThat(isAuthenticated).isTrue(); + } + + @Test + void testAnonymousIsNotAuthenticated() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("anonymous", "anonymous", authorities)); + SecurityContextHolder.setContext(securityContext); + boolean isAuthenticated = SecurityUtils.isAuthenticated(); + assertThat(isAuthenticated).isFalse(); + } + + @Test + void testHasCurrentUserThisAuthority() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("user", "user", authorities)); + SecurityContextHolder.setContext(securityContext); + + assertThat(SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.USER)).isTrue(); + assertThat(SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.ADMIN)).isFalse(); + } + + @Test + void testHasCurrentUserAnyOfAuthorities() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("user", "user", authorities)); + SecurityContextHolder.setContext(securityContext); + + assertThat(SecurityUtils.hasCurrentUserAnyOfAuthorities(AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN)).isTrue(); + assertThat(SecurityUtils.hasCurrentUserAnyOfAuthorities(AuthoritiesConstants.ANONYMOUS, AuthoritiesConstants.ADMIN)).isFalse(); + } + + @Test + void testHasCurrentUserNoneOfAuthorities() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("user", "user", authorities)); + SecurityContextHolder.setContext(securityContext); + + assertThat(SecurityUtils.hasCurrentUserNoneOfAuthorities(AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN)).isFalse(); + assertThat(SecurityUtils.hasCurrentUserNoneOfAuthorities(AuthoritiesConstants.ANONYMOUS, AuthoritiesConstants.ADMIN)).isTrue(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/MailServiceIT.java b/src/test/java/com/oguerreiro/resilient/service/MailServiceIT.java new file mode 100644 index 0000000..dad3009 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/MailServiceIT.java @@ -0,0 +1,246 @@ +package com.oguerreiro.resilient.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mail.MailSendException; +import org.springframework.mail.javamail.JavaMailSender; + +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.config.Constants; +import com.oguerreiro.resilient.domain.User; + +import jakarta.mail.Multipart; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import tech.jhipster.config.JHipsterProperties; + +/** + * Integration tests for {@link MailService}. + */ +@IntegrationTest +class MailServiceIT { + + private static final String[] languages = { + "pt-pt", + "en", + // jhipster-needle-i18n-language-constant - JHipster will add/remove languages in this array + }; + private static final Pattern PATTERN_LOCALE_3 = Pattern.compile("([a-z]{2})-([a-zA-Z]{4})-([a-z]{2})"); + private static final Pattern PATTERN_LOCALE_2 = Pattern.compile("([a-z]{2})-([a-z]{2})"); + + @Autowired + private JHipsterProperties jHipsterProperties; + + @MockBean + private JavaMailSender javaMailSender; + + @Captor + private ArgumentCaptor messageCaptor; + + @Autowired + private MailService mailService; + + @BeforeEach + public void setup() { + doNothing().when(javaMailSender).send(any(MimeMessage.class)); + when(javaMailSender.createMimeMessage()).thenReturn(new MimeMessage((Session) null)); + } + + @Test + void testSendEmail() throws Exception { + mailService.sendEmail("john.doe@example.com", "testSubject", "testContent", false, false); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + assertThat(message.getSubject()).isEqualTo("testSubject"); + assertThat(message.getAllRecipients()[0]).hasToString("john.doe@example.com"); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent()).isInstanceOf(String.class); + assertThat(message.getContent()).hasToString("testContent"); + assertThat(message.getDataHandler().getContentType()).isEqualTo("text/plain; charset=UTF-8"); + } + + @Test + void testSendHtmlEmail() throws Exception { + mailService.sendEmail("john.doe@example.com", "testSubject", "testContent", false, true); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + assertThat(message.getSubject()).isEqualTo("testSubject"); + assertThat(message.getAllRecipients()[0]).hasToString("john.doe@example.com"); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent()).isInstanceOf(String.class); + assertThat(message.getContent()).hasToString("testContent"); + assertThat(message.getDataHandler().getContentType()).isEqualTo("text/html;charset=UTF-8"); + } + + @Test + void testSendMultipartEmail() throws Exception { + mailService.sendEmail("john.doe@example.com", "testSubject", "testContent", true, false); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + MimeMultipart mp = (MimeMultipart) message.getContent(); + MimeBodyPart part = (MimeBodyPart) ((MimeMultipart) mp.getBodyPart(0).getContent()).getBodyPart(0); + ByteArrayOutputStream aos = new ByteArrayOutputStream(); + part.writeTo(aos); + assertThat(message.getSubject()).isEqualTo("testSubject"); + assertThat(message.getAllRecipients()[0]).hasToString("john.doe@example.com"); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent()).isInstanceOf(Multipart.class); + assertThat(aos).hasToString("\r\ntestContent"); + assertThat(part.getDataHandler().getContentType()).isEqualTo("text/plain; charset=UTF-8"); + } + + @Test + void testSendMultipartHtmlEmail() throws Exception { + mailService.sendEmail("john.doe@example.com", "testSubject", "testContent", true, true); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + MimeMultipart mp = (MimeMultipart) message.getContent(); + MimeBodyPart part = (MimeBodyPart) ((MimeMultipart) mp.getBodyPart(0).getContent()).getBodyPart(0); + ByteArrayOutputStream aos = new ByteArrayOutputStream(); + part.writeTo(aos); + assertThat(message.getSubject()).isEqualTo("testSubject"); + assertThat(message.getAllRecipients()[0]).hasToString("john.doe@example.com"); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent()).isInstanceOf(Multipart.class); + assertThat(aos).hasToString("\r\ntestContent"); + assertThat(part.getDataHandler().getContentType()).isEqualTo("text/html;charset=UTF-8"); + } + + @Test + void testSendEmailFromTemplate() throws Exception { + User user = new User(); + user.setLangKey(Constants.DEFAULT_LANGUAGE); + user.setLogin("john"); + user.setEmail("john.doe@example.com"); + mailService.sendEmailFromTemplate(user, "mail/testEmail", "email.test.title"); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + assertThat(message.getSubject()).isEqualTo("test title"); + assertThat(message.getAllRecipients()[0]).hasToString(user.getEmail()); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent().toString()).isEqualToNormalizingNewlines("test title, http://127.0.0.1:8080, john\n"); + assertThat(message.getDataHandler().getContentType()).isEqualTo("text/html;charset=UTF-8"); + } + + @Test + void testSendActivationEmail() throws Exception { + User user = new User(); + user.setLangKey(Constants.DEFAULT_LANGUAGE); + user.setLogin("john"); + user.setEmail("john.doe@example.com"); + mailService.sendActivationEmail(user); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + assertThat(message.getAllRecipients()[0]).hasToString(user.getEmail()); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent().toString()).isNotEmpty(); + assertThat(message.getDataHandler().getContentType()).isEqualTo("text/html;charset=UTF-8"); + } + + @Test + void testCreationEmail() throws Exception { + User user = new User(); + user.setLangKey(Constants.DEFAULT_LANGUAGE); + user.setLogin("john"); + user.setEmail("john.doe@example.com"); + mailService.sendCreationEmail(user); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + assertThat(message.getAllRecipients()[0]).hasToString(user.getEmail()); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent().toString()).isNotEmpty(); + assertThat(message.getDataHandler().getContentType()).isEqualTo("text/html;charset=UTF-8"); + } + + @Test + void testSendPasswordResetMail() throws Exception { + User user = new User(); + user.setLangKey(Constants.DEFAULT_LANGUAGE); + user.setLogin("john"); + user.setEmail("john.doe@example.com"); + mailService.sendPasswordResetMail(user); + verify(javaMailSender).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + assertThat(message.getAllRecipients()[0]).hasToString(user.getEmail()); + assertThat(message.getFrom()[0]).hasToString(jHipsterProperties.getMail().getFrom()); + assertThat(message.getContent().toString()).isNotEmpty(); + assertThat(message.getDataHandler().getContentType()).isEqualTo("text/html;charset=UTF-8"); + } + + @Test + void testSendEmailWithException() { + doThrow(MailSendException.class).when(javaMailSender).send(any(MimeMessage.class)); + try { + mailService.sendEmail("john.doe@example.com", "testSubject", "testContent", false, false); + } catch (Exception e) { + fail("Exception shouldn't have been thrown"); + } + } + + @Test + void testSendLocalizedEmailForAllSupportedLanguages() throws Exception { + User user = new User(); + user.setLogin("john"); + user.setEmail("john.doe@example.com"); + for (String langKey : languages) { + user.setLangKey(langKey); + mailService.sendEmailFromTemplate(user, "mail/testEmail", "email.test.title"); + verify(javaMailSender, atLeastOnce()).send(messageCaptor.capture()); + MimeMessage message = messageCaptor.getValue(); + + String propertyFilePath = "i18n/messages_" + getMessageSourceSuffixForLanguage(langKey) + ".properties"; + URL resource = this.getClass().getClassLoader().getResource(propertyFilePath); + File file = new File(new URI(resource.getFile()).getPath()); + Properties properties = new Properties(); + properties.load(new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8"))); + + String emailTitle = (String) properties.get("email.test.title"); + assertThat(message.getSubject()).isEqualTo(emailTitle); + assertThat(message.getContent().toString()).isEqualToNormalizingNewlines( + "" + emailTitle + ", http://127.0.0.1:8080, john\n" + ); + } + } + + /** + * Convert a lang key to the Java locale. + */ + private String getMessageSourceSuffixForLanguage(String langKey) { + String javaLangKey = langKey; + Matcher matcher2 = PATTERN_LOCALE_2.matcher(langKey); + if (matcher2.matches()) { + javaLangKey = matcher2.group(1) + "_" + matcher2.group(2).toUpperCase(); + } + Matcher matcher3 = PATTERN_LOCALE_3.matcher(langKey); + if (matcher3.matches()) { + javaLangKey = matcher3.group(1) + "_" + matcher3.group(2) + "_" + matcher3.group(3).toUpperCase(); + } + return javaLangKey; + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/UserServiceIT.java b/src/test/java/com/oguerreiro/resilient/service/UserServiceIT.java new file mode 100644 index 0000000..a55c587 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/UserServiceIT.java @@ -0,0 +1,239 @@ +package com.oguerreiro.resilient.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.PersistentToken; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.PersistentTokenRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.transaction.annotation.Transactional; +import tech.jhipster.security.RandomUtil; + +/** + * Integration tests for {@link UserService}. + */ +@IntegrationTest +@Transactional +class UserServiceIT { + + private static final String DEFAULT_LOGIN = "johndoe_service"; + + private static final String DEFAULT_EMAIL = "johndoe_service@localhost"; + + private static final String DEFAULT_FIRSTNAME = "john"; + + private static final String DEFAULT_LASTNAME = "doe"; + + @Autowired + private CacheManager cacheManager; + + private static final String DEFAULT_IMAGEURL = "http://placehold.it/50x50"; + + private static final String DEFAULT_LANGKEY = "dummy"; + + @Autowired + private PersistentTokenRepository persistentTokenRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + private AuditingHandler auditingHandler; + + @MockBean + private DateTimeProvider dateTimeProvider; + + private User user; + + private Long numberOfUsers; + + @BeforeEach + public void countUsers() { + numberOfUsers = userRepository.count(); + } + + @BeforeEach + public void init() { + user = new User(); + user.setLogin(DEFAULT_LOGIN); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + user.setEmail(DEFAULT_EMAIL); + user.setFirstName(DEFAULT_FIRSTNAME); + user.setLastName(DEFAULT_LASTNAME); + user.setImageUrl(DEFAULT_IMAGEURL); + user.setLangKey(DEFAULT_LANGKEY); + + when(dateTimeProvider.getNow()).thenReturn(Optional.of(LocalDateTime.now())); + auditingHandler.setDateTimeProvider(dateTimeProvider); + } + + @AfterEach + public void cleanupAndCheck() { + cacheManager + .getCacheNames() + .stream() + .map(cacheName -> this.cacheManager.getCache(cacheName)) + .filter(Objects::nonNull) + .forEach(Cache::clear); + persistentTokenRepository.deleteAll(); + userService.deleteUser(DEFAULT_LOGIN); + assertThat(userRepository.count()).isEqualTo(numberOfUsers); + numberOfUsers = null; + } + + @Test + @Transactional + void testRemoveOldPersistentTokens() { + userRepository.saveAndFlush(user); + int existingCount = persistentTokenRepository.findByUser(user).size(); + LocalDate today = LocalDate.now(); + generateUserToken(user, "1111-1111", today); + generateUserToken(user, "2222-2222", today.minusDays(32)); + assertThat(persistentTokenRepository.findByUser(user)).hasSize(existingCount + 2); + userService.removeOldPersistentTokens(); + assertThat(persistentTokenRepository.findByUser(user)).hasSize(existingCount + 1); + } + + @Test + @Transactional + void assertThatUserMustExistToResetPassword() { + userRepository.saveAndFlush(user); + Optional maybeUser = userService.requestPasswordReset("invalid.login@localhost"); + assertThat(maybeUser).isNotPresent(); + + maybeUser = userService.requestPasswordReset(user.getEmail()); + assertThat(maybeUser).isPresent(); + assertThat(maybeUser.orElse(null).getEmail()).isEqualTo(user.getEmail()); + assertThat(maybeUser.orElse(null).getResetDate()).isNotNull(); + assertThat(maybeUser.orElse(null).getResetKey()).isNotNull(); + } + + @Test + @Transactional + void assertThatOnlyActivatedUserCanRequestPasswordReset() { + user.setActivated(false); + userRepository.saveAndFlush(user); + + Optional maybeUser = userService.requestPasswordReset(user.getLogin()); + assertThat(maybeUser).isNotPresent(); + userRepository.delete(user); + } + + @Test + @Transactional + void assertThatResetKeyMustNotBeOlderThan24Hours() { + Instant daysAgo = Instant.now().minus(25, ChronoUnit.HOURS); + String resetKey = RandomUtil.generateResetKey(); + user.setActivated(true); + user.setResetDate(daysAgo); + user.setResetKey(resetKey); + userRepository.saveAndFlush(user); + + Optional maybeUser = userService.completePasswordReset("johndoe2", user.getResetKey()); + assertThat(maybeUser).isNotPresent(); + userRepository.delete(user); + } + + @Test + @Transactional + void assertThatResetKeyMustBeValid() { + Instant daysAgo = Instant.now().minus(25, ChronoUnit.HOURS); + user.setActivated(true); + user.setResetDate(daysAgo); + user.setResetKey("1234"); + userRepository.saveAndFlush(user); + + Optional maybeUser = userService.completePasswordReset("johndoe2", user.getResetKey()); + assertThat(maybeUser).isNotPresent(); + userRepository.delete(user); + } + + @Test + @Transactional + void assertThatUserCanResetPassword() { + String oldPassword = user.getPassword(); + Instant daysAgo = Instant.now().minus(2, ChronoUnit.HOURS); + String resetKey = RandomUtil.generateResetKey(); + user.setActivated(true); + user.setResetDate(daysAgo); + user.setResetKey(resetKey); + userRepository.saveAndFlush(user); + + Optional maybeUser = userService.completePasswordReset("johndoe2", user.getResetKey()); + assertThat(maybeUser).isPresent(); + assertThat(maybeUser.orElse(null).getResetDate()).isNull(); + assertThat(maybeUser.orElse(null).getResetKey()).isNull(); + assertThat(maybeUser.orElse(null).getPassword()).isNotEqualTo(oldPassword); + + userRepository.delete(user); + } + + @Test + @Transactional + void assertThatNotActivatedUsersWithNotNullActivationKeyCreatedBefore3DaysAreDeleted() { + Instant now = Instant.now(); + when(dateTimeProvider.getNow()).thenReturn(Optional.of(now.minus(4, ChronoUnit.DAYS))); + user.setActivated(false); + user.setActivationKey(RandomStringUtils.random(20)); + User dbUser = userRepository.saveAndFlush(user); + dbUser.setCreatedDate(now.minus(4, ChronoUnit.DAYS)); + userRepository.saveAndFlush(user); + Instant threeDaysAgo = now.minus(3, ChronoUnit.DAYS); + List users = userRepository.findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(threeDaysAgo); + assertThat(users).isNotEmpty(); + userService.removeNotActivatedUsers(); + users = userRepository.findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(threeDaysAgo); + assertThat(users).isEmpty(); + } + + @Test + @Transactional + void assertThatNotActivatedUsersWithNullActivationKeyCreatedBefore3DaysAreNotDeleted() { + Instant now = Instant.now(); + when(dateTimeProvider.getNow()).thenReturn(Optional.of(now.minus(4, ChronoUnit.DAYS))); + user.setActivated(false); + User dbUser = userRepository.saveAndFlush(user); + dbUser.setCreatedDate(now.minus(4, ChronoUnit.DAYS)); + userRepository.saveAndFlush(user); + Instant threeDaysAgo = now.minus(3, ChronoUnit.DAYS); + List users = userRepository.findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(threeDaysAgo); + assertThat(users).isEmpty(); + userService.removeNotActivatedUsers(); + Optional maybeDbUser = userRepository.findById(dbUser.getId()); + assertThat(maybeDbUser).contains(dbUser); + } + + private void generateUserToken(User user, String tokenSeries, LocalDate localDate) { + PersistentToken token = new PersistentToken(); + token.setSeries(tokenSeries); + token.setUser(user); + token.setTokenValue(tokenSeries + "-data"); + token.setTokenDate(localDate); + token.setIpAddress("127.0.0.1"); + token.setUserAgent("Test agent"); + persistentTokenRepository.saveAndFlush(token); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/criteria/VariableCriteriaTest.java b/src/test/java/com/oguerreiro/resilient/service/criteria/VariableCriteriaTest.java new file mode 100644 index 0000000..c861399 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/criteria/VariableCriteriaTest.java @@ -0,0 +1,137 @@ +package com.oguerreiro.resilient.service.criteria; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.BiFunction; +import java.util.function.Function; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +class VariableCriteriaTest { + + @Test + void newVariableCriteriaHasAllFiltersNullTest() { + var variableCriteria = new VariableCriteria(); + assertThat(variableCriteria).is(criteriaFiltersAre(filter -> filter == null)); + } + + @Test + void variableCriteriaFluentMethodsCreatesFiltersTest() { + var variableCriteria = new VariableCriteria(); + + setAllFilters(variableCriteria); + + assertThat(variableCriteria).is(criteriaFiltersAre(filter -> filter != null)); + } + + @Test + void variableCriteriaCopyCreatesNullFilterTest() { + var variableCriteria = new VariableCriteria(); + var copy = variableCriteria.copy(); + + assertThat(variableCriteria).satisfies( + criteria -> + assertThat(criteria).is( + copyFiltersAre(copy, (a, b) -> (a == null || a instanceof Boolean) ? a == b : (a != b && a.equals(b))) + ), + criteria -> assertThat(criteria).isEqualTo(copy), + criteria -> assertThat(criteria).hasSameHashCodeAs(copy) + ); + + assertThat(copy).satisfies( + criteria -> assertThat(criteria).is(criteriaFiltersAre(filter -> filter == null)), + criteria -> assertThat(criteria).isEqualTo(variableCriteria) + ); + } + + @Test + void variableCriteriaCopyDuplicatesEveryExistingFilterTest() { + var variableCriteria = new VariableCriteria(); + setAllFilters(variableCriteria); + + var copy = variableCriteria.copy(); + + assertThat(variableCriteria).satisfies( + criteria -> + assertThat(criteria).is( + copyFiltersAre(copy, (a, b) -> (a == null || a instanceof Boolean) ? a == b : (a != b && a.equals(b))) + ), + criteria -> assertThat(criteria).isEqualTo(copy), + criteria -> assertThat(criteria).hasSameHashCodeAs(copy) + ); + + assertThat(copy).satisfies( + criteria -> assertThat(criteria).is(criteriaFiltersAre(filter -> filter != null)), + criteria -> assertThat(criteria).isEqualTo(variableCriteria) + ); + } + + @Test + void toStringVerifier() { + var variableCriteria = new VariableCriteria(); + + assertThat(variableCriteria).hasToString("VariableCriteria{}"); + } + + private static void setAllFilters(VariableCriteria variableCriteria) { + variableCriteria.id(); + variableCriteria.code(); + variableCriteria.variableIndex(); + variableCriteria.name(); + variableCriteria.description(); + variableCriteria.input(); + variableCriteria.inputMode(); + variableCriteria.output(); + variableCriteria.outputFormula(); + variableCriteria.version(); + variableCriteria.variableUnitsId(); + variableCriteria.baseUnitId(); + variableCriteria.variableScopeId(); + variableCriteria.variableCategoryId(); + variableCriteria.distinct(); + } + + private static Condition criteriaFiltersAre(Function condition) { + return new Condition<>( + criteria -> + condition.apply(criteria.getId()) && + condition.apply(criteria.getCode()) && + condition.apply(criteria.getVariableIndex()) && + condition.apply(criteria.getName()) && + condition.apply(criteria.getDescription()) && + condition.apply(criteria.getInput()) && + condition.apply(criteria.getInputMode()) && + condition.apply(criteria.getOutput()) && + condition.apply(criteria.getOutputFormula()) && + condition.apply(criteria.getVersion()) && + condition.apply(criteria.getVariableUnitsId()) && + condition.apply(criteria.getBaseUnitId()) && + condition.apply(criteria.getVariableScopeId()) && + condition.apply(criteria.getVariableCategoryId()) && + condition.apply(criteria.getDistinct()), + "every filter matches" + ); + } + + private static Condition copyFiltersAre(VariableCriteria copy, BiFunction condition) { + return new Condition<>( + criteria -> + condition.apply(criteria.getId(), copy.getId()) && + condition.apply(criteria.getCode(), copy.getCode()) && + condition.apply(criteria.getVariableIndex(), copy.getVariableIndex()) && + condition.apply(criteria.getName(), copy.getName()) && + condition.apply(criteria.getDescription(), copy.getDescription()) && + condition.apply(criteria.getInput(), copy.getInput()) && + condition.apply(criteria.getInputMode(), copy.getInputMode()) && + condition.apply(criteria.getOutput(), copy.getOutput()) && + condition.apply(criteria.getOutputFormula(), copy.getOutputFormula()) && + condition.apply(criteria.getVersion(), copy.getVersion()) && + condition.apply(criteria.getVariableUnitsId(), copy.getVariableUnitsId()) && + condition.apply(criteria.getBaseUnitId(), copy.getBaseUnitId()) && + condition.apply(criteria.getVariableScopeId(), copy.getVariableScopeId()) && + condition.apply(criteria.getVariableCategoryId(), copy.getVariableCategoryId()) && + condition.apply(criteria.getDistinct(), copy.getDistinct()), + "every filter matches" + ); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/InputDataDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/InputDataDTOTest.java new file mode 100644 index 0000000..41f3ac4 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/InputDataDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class InputDataDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(InputDataDTO.class); + InputDataDTO inputDataDTO1 = new InputDataDTO(); + inputDataDTO1.setId(1L); + InputDataDTO inputDataDTO2 = new InputDataDTO(); + assertThat(inputDataDTO1).isNotEqualTo(inputDataDTO2); + inputDataDTO2.setId(inputDataDTO1.getId()); + assertThat(inputDataDTO1).isEqualTo(inputDataDTO2); + inputDataDTO2.setId(2L); + assertThat(inputDataDTO1).isNotEqualTo(inputDataDTO2); + inputDataDTO1.setId(null); + assertThat(inputDataDTO1).isNotEqualTo(inputDataDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/InputDataUploadDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/InputDataUploadDTOTest.java new file mode 100644 index 0000000..a2d3ce5 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/InputDataUploadDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class InputDataUploadDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(InputDataUploadDTO.class); + InputDataUploadDTO inputDataUploadDTO1 = new InputDataUploadDTO(); + inputDataUploadDTO1.setId(1L); + InputDataUploadDTO inputDataUploadDTO2 = new InputDataUploadDTO(); + assertThat(inputDataUploadDTO1).isNotEqualTo(inputDataUploadDTO2); + inputDataUploadDTO2.setId(inputDataUploadDTO1.getId()); + assertThat(inputDataUploadDTO1).isEqualTo(inputDataUploadDTO2); + inputDataUploadDTO2.setId(2L); + assertThat(inputDataUploadDTO1).isNotEqualTo(inputDataUploadDTO2); + inputDataUploadDTO1.setId(null); + assertThat(inputDataUploadDTO1).isNotEqualTo(inputDataUploadDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/InputDataUploadLogDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/InputDataUploadLogDTOTest.java new file mode 100644 index 0000000..da944de --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/InputDataUploadLogDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class InputDataUploadLogDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(InputDataUploadLogDTO.class); + InputDataUploadLogDTO inputDataUploadLogDTO1 = new InputDataUploadLogDTO(); + inputDataUploadLogDTO1.setId(1L); + InputDataUploadLogDTO inputDataUploadLogDTO2 = new InputDataUploadLogDTO(); + assertThat(inputDataUploadLogDTO1).isNotEqualTo(inputDataUploadLogDTO2); + inputDataUploadLogDTO2.setId(inputDataUploadLogDTO1.getId()); + assertThat(inputDataUploadLogDTO1).isEqualTo(inputDataUploadLogDTO2); + inputDataUploadLogDTO2.setId(2L); + assertThat(inputDataUploadLogDTO1).isNotEqualTo(inputDataUploadLogDTO2); + inputDataUploadLogDTO1.setId(null); + assertThat(inputDataUploadLogDTO1).isNotEqualTo(inputDataUploadLogDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/MetadataPropertyDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/MetadataPropertyDTOTest.java new file mode 100644 index 0000000..1503697 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/MetadataPropertyDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class MetadataPropertyDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(MetadataPropertyDTO.class); + MetadataPropertyDTO metadataPropertyDTO1 = new MetadataPropertyDTO(); + metadataPropertyDTO1.setId(1L); + MetadataPropertyDTO metadataPropertyDTO2 = new MetadataPropertyDTO(); + assertThat(metadataPropertyDTO1).isNotEqualTo(metadataPropertyDTO2); + metadataPropertyDTO2.setId(metadataPropertyDTO1.getId()); + assertThat(metadataPropertyDTO1).isEqualTo(metadataPropertyDTO2); + metadataPropertyDTO2.setId(2L); + assertThat(metadataPropertyDTO1).isNotEqualTo(metadataPropertyDTO2); + metadataPropertyDTO1.setId(null); + assertThat(metadataPropertyDTO1).isNotEqualTo(metadataPropertyDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/MetadataValueDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/MetadataValueDTOTest.java new file mode 100644 index 0000000..9b6d9d1 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/MetadataValueDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class MetadataValueDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(MetadataValueDTO.class); + MetadataValueDTO metadataValueDTO1 = new MetadataValueDTO(); + metadataValueDTO1.setId(1L); + MetadataValueDTO metadataValueDTO2 = new MetadataValueDTO(); + assertThat(metadataValueDTO1).isNotEqualTo(metadataValueDTO2); + metadataValueDTO2.setId(metadataValueDTO1.getId()); + assertThat(metadataValueDTO1).isEqualTo(metadataValueDTO2); + metadataValueDTO2.setId(2L); + assertThat(metadataValueDTO1).isNotEqualTo(metadataValueDTO2); + metadataValueDTO1.setId(null); + assertThat(metadataValueDTO1).isNotEqualTo(metadataValueDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/OrganizationDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/OrganizationDTOTest.java new file mode 100644 index 0000000..6fb912d --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/OrganizationDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class OrganizationDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(OrganizationDTO.class); + OrganizationDTO organizationDTO1 = new OrganizationDTO(); + organizationDTO1.setId(1L); + OrganizationDTO organizationDTO2 = new OrganizationDTO(); + assertThat(organizationDTO1).isNotEqualTo(organizationDTO2); + organizationDTO2.setId(organizationDTO1.getId()); + assertThat(organizationDTO1).isEqualTo(organizationDTO2); + organizationDTO2.setId(2L); + assertThat(organizationDTO1).isNotEqualTo(organizationDTO2); + organizationDTO1.setId(null); + assertThat(organizationDTO1).isNotEqualTo(organizationDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/OrganizationTypeDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/OrganizationTypeDTOTest.java new file mode 100644 index 0000000..e0d226d --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/OrganizationTypeDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class OrganizationTypeDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(OrganizationTypeDTO.class); + OrganizationTypeDTO organizationTypeDTO1 = new OrganizationTypeDTO(); + organizationTypeDTO1.setId(1L); + OrganizationTypeDTO organizationTypeDTO2 = new OrganizationTypeDTO(); + assertThat(organizationTypeDTO1).isNotEqualTo(organizationTypeDTO2); + organizationTypeDTO2.setId(organizationTypeDTO1.getId()); + assertThat(organizationTypeDTO1).isEqualTo(organizationTypeDTO2); + organizationTypeDTO2.setId(2L); + assertThat(organizationTypeDTO1).isNotEqualTo(organizationTypeDTO2); + organizationTypeDTO1.setId(null); + assertThat(organizationTypeDTO1).isNotEqualTo(organizationTypeDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/OutputDataDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/OutputDataDTOTest.java new file mode 100644 index 0000000..3cf341a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/OutputDataDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class OutputDataDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(OutputDataDTO.class); + OutputDataDTO outputDataDTO1 = new OutputDataDTO(); + outputDataDTO1.setId(1L); + OutputDataDTO outputDataDTO2 = new OutputDataDTO(); + assertThat(outputDataDTO1).isNotEqualTo(outputDataDTO2); + outputDataDTO2.setId(outputDataDTO1.getId()); + assertThat(outputDataDTO1).isEqualTo(outputDataDTO2); + outputDataDTO2.setId(2L); + assertThat(outputDataDTO1).isNotEqualTo(outputDataDTO2); + outputDataDTO1.setId(null); + assertThat(outputDataDTO1).isNotEqualTo(outputDataDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/PeriodDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/PeriodDTOTest.java new file mode 100644 index 0000000..aada192 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/PeriodDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class PeriodDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(PeriodDTO.class); + PeriodDTO periodDTO1 = new PeriodDTO(); + periodDTO1.setId(1L); + PeriodDTO periodDTO2 = new PeriodDTO(); + assertThat(periodDTO1).isNotEqualTo(periodDTO2); + periodDTO2.setId(periodDTO1.getId()); + assertThat(periodDTO1).isEqualTo(periodDTO2); + periodDTO2.setId(2L); + assertThat(periodDTO1).isNotEqualTo(periodDTO2); + periodDTO1.setId(null); + assertThat(periodDTO1).isNotEqualTo(periodDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/PeriodVersionDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/PeriodVersionDTOTest.java new file mode 100644 index 0000000..225428a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/PeriodVersionDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class PeriodVersionDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(PeriodVersionDTO.class); + PeriodVersionDTO periodVersionDTO1 = new PeriodVersionDTO(); + periodVersionDTO1.setId(1L); + PeriodVersionDTO periodVersionDTO2 = new PeriodVersionDTO(); + assertThat(periodVersionDTO1).isNotEqualTo(periodVersionDTO2); + periodVersionDTO2.setId(periodVersionDTO1.getId()); + assertThat(periodVersionDTO1).isEqualTo(periodVersionDTO2); + periodVersionDTO2.setId(2L); + assertThat(periodVersionDTO1).isNotEqualTo(periodVersionDTO2); + periodVersionDTO1.setId(null); + assertThat(periodVersionDTO1).isNotEqualTo(periodVersionDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/UnitConverterDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/UnitConverterDTOTest.java new file mode 100644 index 0000000..74332d7 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/UnitConverterDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class UnitConverterDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(UnitConverterDTO.class); + UnitConverterDTO unitConverterDTO1 = new UnitConverterDTO(); + unitConverterDTO1.setId(1L); + UnitConverterDTO unitConverterDTO2 = new UnitConverterDTO(); + assertThat(unitConverterDTO1).isNotEqualTo(unitConverterDTO2); + unitConverterDTO2.setId(unitConverterDTO1.getId()); + assertThat(unitConverterDTO1).isEqualTo(unitConverterDTO2); + unitConverterDTO2.setId(2L); + assertThat(unitConverterDTO1).isNotEqualTo(unitConverterDTO2); + unitConverterDTO1.setId(null); + assertThat(unitConverterDTO1).isNotEqualTo(unitConverterDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/UnitDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/UnitDTOTest.java new file mode 100644 index 0000000..ab41213 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/UnitDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class UnitDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(UnitDTO.class); + UnitDTO unitDTO1 = new UnitDTO(); + unitDTO1.setId(1L); + UnitDTO unitDTO2 = new UnitDTO(); + assertThat(unitDTO1).isNotEqualTo(unitDTO2); + unitDTO2.setId(unitDTO1.getId()); + assertThat(unitDTO1).isEqualTo(unitDTO2); + unitDTO2.setId(2L); + assertThat(unitDTO1).isNotEqualTo(unitDTO2); + unitDTO1.setId(null); + assertThat(unitDTO1).isNotEqualTo(unitDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/UnitTypeDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/UnitTypeDTOTest.java new file mode 100644 index 0000000..c6ca364 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/UnitTypeDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class UnitTypeDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(UnitTypeDTO.class); + UnitTypeDTO unitTypeDTO1 = new UnitTypeDTO(); + unitTypeDTO1.setId(1L); + UnitTypeDTO unitTypeDTO2 = new UnitTypeDTO(); + assertThat(unitTypeDTO1).isNotEqualTo(unitTypeDTO2); + unitTypeDTO2.setId(unitTypeDTO1.getId()); + assertThat(unitTypeDTO1).isEqualTo(unitTypeDTO2); + unitTypeDTO2.setId(2L); + assertThat(unitTypeDTO1).isNotEqualTo(unitTypeDTO2); + unitTypeDTO1.setId(null); + assertThat(unitTypeDTO1).isNotEqualTo(unitTypeDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/VariableCategoryDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/VariableCategoryDTOTest.java new file mode 100644 index 0000000..e9f8d6e --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/VariableCategoryDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class VariableCategoryDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableCategoryDTO.class); + VariableCategoryDTO variableCategoryDTO1 = new VariableCategoryDTO(); + variableCategoryDTO1.setId(1L); + VariableCategoryDTO variableCategoryDTO2 = new VariableCategoryDTO(); + assertThat(variableCategoryDTO1).isNotEqualTo(variableCategoryDTO2); + variableCategoryDTO2.setId(variableCategoryDTO1.getId()); + assertThat(variableCategoryDTO1).isEqualTo(variableCategoryDTO2); + variableCategoryDTO2.setId(2L); + assertThat(variableCategoryDTO1).isNotEqualTo(variableCategoryDTO2); + variableCategoryDTO1.setId(null); + assertThat(variableCategoryDTO1).isNotEqualTo(variableCategoryDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/VariableClassDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/VariableClassDTOTest.java new file mode 100644 index 0000000..4438282 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/VariableClassDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class VariableClassDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableClassDTO.class); + VariableClassDTO variableClassDTO1 = new VariableClassDTO(); + variableClassDTO1.setId(1L); + VariableClassDTO variableClassDTO2 = new VariableClassDTO(); + assertThat(variableClassDTO1).isNotEqualTo(variableClassDTO2); + variableClassDTO2.setId(variableClassDTO1.getId()); + assertThat(variableClassDTO1).isEqualTo(variableClassDTO2); + variableClassDTO2.setId(2L); + assertThat(variableClassDTO1).isNotEqualTo(variableClassDTO2); + variableClassDTO1.setId(null); + assertThat(variableClassDTO1).isNotEqualTo(variableClassDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/VariableDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/VariableDTOTest.java new file mode 100644 index 0000000..bccfbed --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/VariableDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class VariableDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableDTO.class); + VariableDTO variableDTO1 = new VariableDTO(); + variableDTO1.setId(1L); + VariableDTO variableDTO2 = new VariableDTO(); + assertThat(variableDTO1).isNotEqualTo(variableDTO2); + variableDTO2.setId(variableDTO1.getId()); + assertThat(variableDTO1).isEqualTo(variableDTO2); + variableDTO2.setId(2L); + assertThat(variableDTO1).isNotEqualTo(variableDTO2); + variableDTO1.setId(null); + assertThat(variableDTO1).isNotEqualTo(variableDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/VariableScopeDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/VariableScopeDTOTest.java new file mode 100644 index 0000000..253b364 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/VariableScopeDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class VariableScopeDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableScopeDTO.class); + VariableScopeDTO variableScopeDTO1 = new VariableScopeDTO(); + variableScopeDTO1.setId(1L); + VariableScopeDTO variableScopeDTO2 = new VariableScopeDTO(); + assertThat(variableScopeDTO1).isNotEqualTo(variableScopeDTO2); + variableScopeDTO2.setId(variableScopeDTO1.getId()); + assertThat(variableScopeDTO1).isEqualTo(variableScopeDTO2); + variableScopeDTO2.setId(2L); + assertThat(variableScopeDTO1).isNotEqualTo(variableScopeDTO2); + variableScopeDTO1.setId(null); + assertThat(variableScopeDTO1).isNotEqualTo(variableScopeDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/dto/VariableUnitsDTOTest.java b/src/test/java/com/oguerreiro/resilient/service/dto/VariableUnitsDTOTest.java new file mode 100644 index 0000000..3ee7a0e --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/dto/VariableUnitsDTOTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.web.rest.TestUtil; +import org.junit.jupiter.api.Test; + +class VariableUnitsDTOTest { + + @Test + void dtoEqualsVerifier() throws Exception { + TestUtil.equalsVerifier(VariableUnitsDTO.class); + VariableUnitsDTO variableUnitsDTO1 = new VariableUnitsDTO(); + variableUnitsDTO1.setId(1L); + VariableUnitsDTO variableUnitsDTO2 = new VariableUnitsDTO(); + assertThat(variableUnitsDTO1).isNotEqualTo(variableUnitsDTO2); + variableUnitsDTO2.setId(variableUnitsDTO1.getId()); + assertThat(variableUnitsDTO1).isEqualTo(variableUnitsDTO2); + variableUnitsDTO2.setId(2L); + assertThat(variableUnitsDTO1).isNotEqualTo(variableUnitsDTO2); + variableUnitsDTO1.setId(null); + assertThat(variableUnitsDTO1).isNotEqualTo(variableUnitsDTO2); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataMapperTest.java new file mode 100644 index 0000000..a3844ea --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.InputDataAsserts.*; +import static com.oguerreiro.resilient.domain.InputDataTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class InputDataMapperTest { + + private InputDataMapper inputDataMapper; + + @BeforeEach + void setUp() { + inputDataMapper = new InputDataMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getInputDataSample1(); + var actual = inputDataMapper.toEntity(inputDataMapper.toDto(expected)); + assertInputDataAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataUploadLogMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataUploadLogMapperTest.java new file mode 100644 index 0000000..b5758c6 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataUploadLogMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.InputDataUploadLogAsserts.*; +import static com.oguerreiro.resilient.domain.InputDataUploadLogTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class InputDataUploadLogMapperTest { + + private InputDataUploadLogMapper inputDataUploadLogMapper; + + @BeforeEach + void setUp() { + inputDataUploadLogMapper = new InputDataUploadLogMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getInputDataUploadLogSample1(); + var actual = inputDataUploadLogMapper.toEntity(inputDataUploadLogMapper.toDto(expected)); + assertInputDataUploadLogAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataUploadMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataUploadMapperTest.java new file mode 100644 index 0000000..5a2049b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/InputDataUploadMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.InputDataUploadAsserts.*; +import static com.oguerreiro.resilient.domain.InputDataUploadTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class InputDataUploadMapperTest { + + private InputDataUploadMapper inputDataUploadMapper; + + @BeforeEach + void setUp() { + inputDataUploadMapper = new InputDataUploadMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getInputDataUploadSample1(); + var actual = inputDataUploadMapper.toEntity(inputDataUploadMapper.toDto(expected)); + assertInputDataUploadAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/MetadataPropertyMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/MetadataPropertyMapperTest.java new file mode 100644 index 0000000..58c3428 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/MetadataPropertyMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.MetadataPropertyAsserts.*; +import static com.oguerreiro.resilient.domain.MetadataPropertyTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MetadataPropertyMapperTest { + + private MetadataPropertyMapper metadataPropertyMapper; + + @BeforeEach + void setUp() { + metadataPropertyMapper = new MetadataPropertyMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getMetadataPropertySample1(); + var actual = metadataPropertyMapper.toEntity(metadataPropertyMapper.toDto(expected)); + assertMetadataPropertyAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/MetadataValueMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/MetadataValueMapperTest.java new file mode 100644 index 0000000..25b102c --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/MetadataValueMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.MetadataValueAsserts.*; +import static com.oguerreiro.resilient.domain.MetadataValueTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MetadataValueMapperTest { + + private MetadataValueMapper metadataValueMapper; + + @BeforeEach + void setUp() { + metadataValueMapper = new MetadataValueMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getMetadataValueSample1(); + var actual = metadataValueMapper.toEntity(metadataValueMapper.toDto(expected)); + assertMetadataValueAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/OrganizationMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/OrganizationMapperTest.java new file mode 100644 index 0000000..9037577 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/OrganizationMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.OrganizationAsserts.*; +import static com.oguerreiro.resilient.domain.OrganizationTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OrganizationMapperTest { + + private OrganizationMapper organizationMapper; + + @BeforeEach + void setUp() { + organizationMapper = new OrganizationMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getOrganizationSample1(); + var actual = organizationMapper.toEntity(organizationMapper.toDto(expected)); + assertOrganizationAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/OrganizationTypeMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/OrganizationTypeMapperTest.java new file mode 100644 index 0000000..3792738 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/OrganizationTypeMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.OrganizationTypeAsserts.*; +import static com.oguerreiro.resilient.domain.OrganizationTypeTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OrganizationTypeMapperTest { + + private OrganizationTypeMapper organizationTypeMapper; + + @BeforeEach + void setUp() { + organizationTypeMapper = new OrganizationTypeMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getOrganizationTypeSample1(); + var actual = organizationTypeMapper.toEntity(organizationTypeMapper.toDto(expected)); + assertOrganizationTypeAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/OutputDataMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/OutputDataMapperTest.java new file mode 100644 index 0000000..03715f8 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/OutputDataMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.OutputDataAsserts.*; +import static com.oguerreiro.resilient.domain.OutputDataTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OutputDataMapperTest { + + private OutputDataMapper outputDataMapper; + + @BeforeEach + void setUp() { + outputDataMapper = new OutputDataMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getOutputDataSample1(); + var actual = outputDataMapper.toEntity(outputDataMapper.toDto(expected)); + assertOutputDataAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/PeriodMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/PeriodMapperTest.java new file mode 100644 index 0000000..4b3d961 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/PeriodMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.PeriodAsserts.*; +import static com.oguerreiro.resilient.domain.PeriodTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PeriodMapperTest { + + private PeriodMapper periodMapper; + + @BeforeEach + void setUp() { + periodMapper = new PeriodMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getPeriodSample1(); + var actual = periodMapper.toEntity(periodMapper.toDto(expected)); + assertPeriodAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/PeriodVersionMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/PeriodVersionMapperTest.java new file mode 100644 index 0000000..cce1b08 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/PeriodVersionMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.PeriodVersionAsserts.*; +import static com.oguerreiro.resilient.domain.PeriodVersionTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PeriodVersionMapperTest { + + private PeriodVersionMapper periodVersionMapper; + + @BeforeEach + void setUp() { + periodVersionMapper = new PeriodVersionMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getPeriodVersionSample1(); + var actual = periodVersionMapper.toEntity(periodVersionMapper.toDto(expected)); + assertPeriodVersionAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/UnitConverterMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/UnitConverterMapperTest.java new file mode 100644 index 0000000..92cecb8 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/UnitConverterMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.UnitConverterAsserts.*; +import static com.oguerreiro.resilient.domain.UnitConverterTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UnitConverterMapperTest { + + private UnitConverterMapper unitConverterMapper; + + @BeforeEach + void setUp() { + unitConverterMapper = new UnitConverterMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getUnitConverterSample1(); + var actual = unitConverterMapper.toEntity(unitConverterMapper.toDto(expected)); + assertUnitConverterAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/UnitMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/UnitMapperTest.java new file mode 100644 index 0000000..3fd1f2a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/UnitMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.UnitAsserts.*; +import static com.oguerreiro.resilient.domain.UnitTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UnitMapperTest { + + private UnitMapper unitMapper; + + @BeforeEach + void setUp() { + unitMapper = new UnitMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getUnitSample1(); + var actual = unitMapper.toEntity(unitMapper.toDto(expected)); + assertUnitAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/UnitTypeMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/UnitTypeMapperTest.java new file mode 100644 index 0000000..b380305 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/UnitTypeMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.UnitTypeAsserts.*; +import static com.oguerreiro.resilient.domain.UnitTypeTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UnitTypeMapperTest { + + private UnitTypeMapper unitTypeMapper; + + @BeforeEach + void setUp() { + unitTypeMapper = new UnitTypeMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getUnitTypeSample1(); + var actual = unitTypeMapper.toEntity(unitTypeMapper.toDto(expected)); + assertUnitTypeAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/UserMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/UserMapperTest.java new file mode 100644 index 0000000..c2edefc --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/UserMapperTest.java @@ -0,0 +1,179 @@ +package com.oguerreiro.resilient.service.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.oguerreiro.resilient.domain.Authority; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import com.oguerreiro.resilient.service.dto.UserDTO; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link UserMapper}. + */ +class UserMapperTest { + + private static final String DEFAULT_LOGIN = "johndoe"; + private static final Long DEFAULT_ID = 1L; + + private UserMapper userMapper; + private User user; + private AdminUserDTO userDto; + + @BeforeEach + public void init() { + userMapper = new UserMapper(); + user = new User(); + user.setLogin(DEFAULT_LOGIN); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + user.setEmail("johndoe@localhost"); + user.setFirstName("john"); + user.setLastName("doe"); + user.setImageUrl("image_url"); + user.setCreatedBy(DEFAULT_LOGIN); + user.setCreatedDate(Instant.now()); + user.setLastModifiedBy(DEFAULT_LOGIN); + user.setLastModifiedDate(Instant.now()); + user.setLangKey("en"); + + Set authorities = new HashSet<>(); + Authority authority = new Authority(); + authority.setName(AuthoritiesConstants.USER); + authorities.add(authority); + user.setAuthorities(authorities); + + userDto = new AdminUserDTO(user); + } + + @Test + void testUserToUserDTO() { + AdminUserDTO convertedUserDto = userMapper.userToAdminUserDTO(user); + + assertThat(convertedUserDto.getId()).isEqualTo(user.getId()); + assertThat(convertedUserDto.getLogin()).isEqualTo(user.getLogin()); + assertThat(convertedUserDto.getFirstName()).isEqualTo(user.getFirstName()); + assertThat(convertedUserDto.getLastName()).isEqualTo(user.getLastName()); + assertThat(convertedUserDto.getEmail()).isEqualTo(user.getEmail()); + assertThat(convertedUserDto.isActivated()).isEqualTo(user.isActivated()); + assertThat(convertedUserDto.getImageUrl()).isEqualTo(user.getImageUrl()); + assertThat(convertedUserDto.getCreatedBy()).isEqualTo(user.getCreatedBy()); + assertThat(convertedUserDto.getCreatedDate()).isEqualTo(user.getCreatedDate()); + assertThat(convertedUserDto.getLastModifiedBy()).isEqualTo(user.getLastModifiedBy()); + assertThat(convertedUserDto.getLastModifiedDate()).isEqualTo(user.getLastModifiedDate()); + assertThat(convertedUserDto.getLangKey()).isEqualTo(user.getLangKey()); + assertThat(convertedUserDto.getAuthorities()).containsExactly(AuthoritiesConstants.USER); + } + + @Test + void testUserDTOtoUser() { + User convertedUser = userMapper.userDTOToUser(userDto); + + assertThat(convertedUser.getId()).isEqualTo(userDto.getId()); + assertThat(convertedUser.getLogin()).isEqualTo(userDto.getLogin()); + assertThat(convertedUser.getFirstName()).isEqualTo(userDto.getFirstName()); + assertThat(convertedUser.getLastName()).isEqualTo(userDto.getLastName()); + assertThat(convertedUser.getEmail()).isEqualTo(userDto.getEmail()); + assertThat(convertedUser.isActivated()).isEqualTo(userDto.isActivated()); + assertThat(convertedUser.getImageUrl()).isEqualTo(userDto.getImageUrl()); + assertThat(convertedUser.getLangKey()).isEqualTo(userDto.getLangKey()); + assertThat(convertedUser.getCreatedBy()).isEqualTo(userDto.getCreatedBy()); + assertThat(convertedUser.getCreatedDate()).isEqualTo(userDto.getCreatedDate()); + assertThat(convertedUser.getLastModifiedBy()).isEqualTo(userDto.getLastModifiedBy()); + assertThat(convertedUser.getLastModifiedDate()).isEqualTo(userDto.getLastModifiedDate()); + assertThat(convertedUser.getAuthorities()).extracting("name").containsExactly(AuthoritiesConstants.USER); + } + + @Test + void usersToUserDTOsShouldMapOnlyNonNullUsers() { + List users = new ArrayList<>(); + users.add(user); + users.add(null); + + List userDTOS = userMapper.usersToUserDTOs(users); + + assertThat(userDTOS).isNotEmpty().size().isEqualTo(1); + } + + @Test + void userDTOsToUsersShouldMapOnlyNonNullUsers() { + List usersDto = new ArrayList<>(); + usersDto.add(userDto); + usersDto.add(null); + + List users = userMapper.userDTOsToUsers(usersDto); + + assertThat(users).isNotEmpty().size().isEqualTo(1); + } + + @Test + void userDTOsToUsersWithAuthoritiesStringShouldMapToUsersWithAuthoritiesDomain() { + Set authoritiesAsString = new HashSet<>(); + authoritiesAsString.add("ADMIN"); + userDto.setAuthorities(authoritiesAsString); + + List usersDto = new ArrayList<>(); + usersDto.add(userDto); + + List users = userMapper.userDTOsToUsers(usersDto); + + assertThat(users).isNotEmpty().size().isEqualTo(1); + assertThat(users.get(0).getAuthorities()).isNotNull(); + assertThat(users.get(0).getAuthorities()).isNotEmpty(); + assertThat(users.get(0).getAuthorities().iterator().next().getName()).isEqualTo("ADMIN"); + } + + @Test + void userDTOsToUsersMapWithNullAuthoritiesStringShouldReturnUserWithEmptyAuthorities() { + userDto.setAuthorities(null); + + List usersDto = new ArrayList<>(); + usersDto.add(userDto); + + List users = userMapper.userDTOsToUsers(usersDto); + + assertThat(users).isNotEmpty().size().isEqualTo(1); + assertThat(users.get(0).getAuthorities()).isNotNull(); + assertThat(users.get(0).getAuthorities()).isEmpty(); + } + + @Test + void userDTOToUserMapWithAuthoritiesStringShouldReturnUserWithAuthorities() { + User convertedUser = userMapper.userDTOToUser(userDto); + + assertThat(convertedUser).isNotNull(); + assertThat(convertedUser.getAuthorities()).isNotNull(); + assertThat(convertedUser.getAuthorities()).isNotEmpty(); + assertThat(convertedUser.getAuthorities().iterator().next().getName()).isEqualTo(AuthoritiesConstants.USER); + } + + @Test + void userDTOToUserMapWithNullAuthoritiesStringShouldReturnUserWithEmptyAuthorities() { + userDto.setAuthorities(null); + + User persistUser = userMapper.userDTOToUser(userDto); + + assertThat(persistUser).isNotNull(); + assertThat(persistUser.getAuthorities()).isNotNull(); + assertThat(persistUser.getAuthorities()).isEmpty(); + } + + @Test + void userDTOToUserMapWithNullUserShouldReturnNull() { + assertThat(userMapper.userDTOToUser(null)).isNull(); + } + + @Test + void testUserFromId() { + assertThat(userMapper.userFromId(DEFAULT_ID).getId()).isEqualTo(DEFAULT_ID); + assertThat(userMapper.userFromId(null)).isNull(); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/VariableCategoryMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableCategoryMapperTest.java new file mode 100644 index 0000000..546ccf2 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableCategoryMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.VariableCategoryAsserts.*; +import static com.oguerreiro.resilient.domain.VariableCategoryTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VariableCategoryMapperTest { + + private VariableCategoryMapper variableCategoryMapper; + + @BeforeEach + void setUp() { + variableCategoryMapper = new VariableCategoryMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getVariableCategorySample1(); + var actual = variableCategoryMapper.toEntity(variableCategoryMapper.toDto(expected)); + assertVariableCategoryAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/VariableClassMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableClassMapperTest.java new file mode 100644 index 0000000..4391a50 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableClassMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.VariableClassAsserts.*; +import static com.oguerreiro.resilient.domain.VariableClassTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VariableClassMapperTest { + + private VariableClassMapper variableClassMapper; + + @BeforeEach + void setUp() { + variableClassMapper = new VariableClassMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getVariableClassSample1(); + var actual = variableClassMapper.toEntity(variableClassMapper.toDto(expected)); + assertVariableClassAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/VariableMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableMapperTest.java new file mode 100644 index 0000000..0195e8b --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.VariableAsserts.*; +import static com.oguerreiro.resilient.domain.VariableTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VariableMapperTest { + + private VariableMapper variableMapper; + + @BeforeEach + void setUp() { + variableMapper = new VariableMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getVariableSample1(); + var actual = variableMapper.toEntity(variableMapper.toDto(expected)); + assertVariableAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/VariableScopeMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableScopeMapperTest.java new file mode 100644 index 0000000..3be3761 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableScopeMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.VariableScopeAsserts.*; +import static com.oguerreiro.resilient.domain.VariableScopeTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VariableScopeMapperTest { + + private VariableScopeMapper variableScopeMapper; + + @BeforeEach + void setUp() { + variableScopeMapper = new VariableScopeMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getVariableScopeSample1(); + var actual = variableScopeMapper.toEntity(variableScopeMapper.toDto(expected)); + assertVariableScopeAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/service/mapper/VariableUnitsMapperTest.java b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableUnitsMapperTest.java new file mode 100644 index 0000000..415d79c --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/service/mapper/VariableUnitsMapperTest.java @@ -0,0 +1,24 @@ +package com.oguerreiro.resilient.service.mapper; + +import static com.oguerreiro.resilient.domain.VariableUnitsAsserts.*; +import static com.oguerreiro.resilient.domain.VariableUnitsTestSamples.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VariableUnitsMapperTest { + + private VariableUnitsMapper variableUnitsMapper; + + @BeforeEach + void setUp() { + variableUnitsMapper = new VariableUnitsMapperImpl(); + } + + @Test + void shouldConvertToDtoAndBack() { + var expected = getVariableUnitsSample1(); + var actual = variableUnitsMapper.toEntity(variableUnitsMapper.toDto(expected)); + assertVariableUnitsAllPropertiesEquals(expected, actual); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/filter/SpaWebFilterIT.java b/src/test/java/com/oguerreiro/resilient/web/filter/SpaWebFilterIT.java new file mode 100644 index 0000000..e07730e --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/filter/SpaWebFilterIT.java @@ -0,0 +1,88 @@ +package com.oguerreiro.resilient.web.filter; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@AutoConfigureMockMvc +@WithMockUser +@IntegrationTest +class SpaWebFilterIT { + + @Autowired + private MockMvc mockMvc; + + @Test + void testFilterForwardsToIndex() throws Exception { + mockMvc.perform(get("/")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void testFilterDoesNotForwardToIndexForApi() throws Exception { + mockMvc.perform(get("/api/authenticate")).andExpect(status().isOk()).andExpect(forwardedUrl(null)); + } + + @Test + @WithMockUser(authorities = AuthoritiesConstants.ADMIN) + void testFilterDoesNotForwardToIndexForV3ApiDocs() throws Exception { + mockMvc.perform(get("/v3/api-docs")).andExpect(status().isOk()).andExpect(forwardedUrl(null)); + } + + @Test + void testFilterDoesNotForwardToIndexForDotFile() throws Exception { + mockMvc.perform(get("/file.js")).andExpect(status().isNotFound()); + } + + @Test + void getBackendEndpoint() throws Exception { + mockMvc.perform(get("/test")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedFirstLevelMapping() throws Exception { + mockMvc.perform(get("/first-level")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedSecondLevelMapping() throws Exception { + mockMvc.perform(get("/first-level/second-level")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedThirdLevelMapping() throws Exception { + mockMvc.perform(get("/first-level/second-level/third-level")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedDeepMapping() throws Exception { + mockMvc.perform(get("/1/2/3/4/5/6/7/8/9/10")).andExpect(forwardedUrl("/index.html")); + } + + @Test + void getUnmappedFirstLevelFile() throws Exception { + mockMvc.perform(get("/foo.js")).andExpect(status().isNotFound()); + } + + /** + * This test verifies that any files that aren't permitted by Spring Security will be forbidden. + * If you want to change this to return isNotFound(), you need to add a request mapping that + * allows this file in SecurityConfiguration. + */ + @Test + void getUnmappedSecondLevelFile() throws Exception { + mockMvc.perform(get("/foo/bar.js")).andExpect(status().isForbidden()); + } + + @Test + void getUnmappedThirdLevelFile() throws Exception { + mockMvc.perform(get("/foo/another/bar.js")).andExpect(status().isForbidden()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/AccountResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/AccountResourceIT.java new file mode 100644 index 0000000..8ac3e12 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/AccountResourceIT.java @@ -0,0 +1,867 @@ +package com.oguerreiro.resilient.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.config.Constants; +import com.oguerreiro.resilient.domain.PersistentToken; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.AuthorityRepository; +import com.oguerreiro.resilient.repository.PersistentTokenRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.service.UserService; +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import com.oguerreiro.resilient.service.dto.PasswordChangeDTO; +import com.oguerreiro.resilient.web.rest.vm.KeyAndPasswordVM; +import com.oguerreiro.resilient.web.rest.vm.ManagedUserVM; +import java.time.Instant; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Stream; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link AccountResource} REST controller. + */ +@AutoConfigureMockMvc +@IntegrationTest +class AccountResourceIT { + + static final String TEST_USER_LOGIN = "test"; + + @Autowired + private ObjectMapper om; + + @Autowired + private UserRepository userRepository; + + @Autowired + private AuthorityRepository authorityRepository; + + @Autowired + private UserService userService; + + @Autowired + private PersistentTokenRepository persistentTokenRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private MockMvc restAccountMockMvc; + + private Long numberOfUsers; + + @BeforeEach + public void countUsers() { + numberOfUsers = userRepository.count(); + } + + @AfterEach + public void cleanupAndCheck() { + assertThat(userRepository.count()).isEqualTo(numberOfUsers); + numberOfUsers = null; + } + + @Test + @WithUnauthenticatedMockUser + void testNonAuthenticatedUser() throws Exception { + restAccountMockMvc + .perform(get("/api/authenticate").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("")); + } + + @Test + @WithMockUser(TEST_USER_LOGIN) + void testAuthenticatedUser() throws Exception { + restAccountMockMvc + .perform(get("/api/authenticate").with(request -> request).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(TEST_USER_LOGIN)); + } + + @Test + @WithMockUser(TEST_USER_LOGIN) + void testGetExistingAccount() throws Exception { + Set authorities = new HashSet<>(); + authorities.add(AuthoritiesConstants.ADMIN); + + AdminUserDTO user = new AdminUserDTO(); + user.setLogin(TEST_USER_LOGIN); + user.setFirstName("john"); + user.setLastName("doe"); + user.setEmail("john.doe@jhipster.com"); + user.setImageUrl("http://placehold.it/50x50"); + user.setLangKey("en"); + user.setAuthorities(authorities); + userService.createUser(user); + + restAccountMockMvc + .perform(get("/api/account").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.login").value(TEST_USER_LOGIN)) + .andExpect(jsonPath("$.firstName").value("john")) + .andExpect(jsonPath("$.lastName").value("doe")) + .andExpect(jsonPath("$.email").value("john.doe@jhipster.com")) + .andExpect(jsonPath("$.imageUrl").value("http://placehold.it/50x50")) + .andExpect(jsonPath("$.langKey").value("en")) + .andExpect(jsonPath("$.authorities").value(AuthoritiesConstants.ADMIN)); + + userService.deleteUser(TEST_USER_LOGIN); + } + + @Test + void testGetUnknownAccount() throws Exception { + restAccountMockMvc.perform(get("/api/account").accept(MediaType.APPLICATION_PROBLEM_JSON)).andExpect(status().isUnauthorized()); + } + + @Test + @Transactional + void testRegisterValid() throws Exception { + ManagedUserVM validUser = new ManagedUserVM(); + validUser.setLogin("test-register-valid"); + validUser.setPassword("password"); + validUser.setFirstName("Alice"); + validUser.setLastName("Test"); + validUser.setEmail("test-register-valid@example.com"); + validUser.setImageUrl("http://placehold.it/50x50"); + validUser.setLangKey(Constants.DEFAULT_LANGUAGE); + validUser.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + assertThat(userRepository.findOneByLogin("test-register-valid")).isEmpty(); + + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(validUser)).with(csrf())) + .andExpect(status().isCreated()); + + assertThat(userRepository.findOneByLogin("test-register-valid")).isPresent(); + + userService.deleteUser("test-register-valid"); + } + + @Test + @Transactional + void testRegisterInvalidLogin() throws Exception { + ManagedUserVM invalidUser = new ManagedUserVM(); + invalidUser.setLogin("funky-log(n"); // <-- invalid + invalidUser.setPassword("password"); + invalidUser.setFirstName("Funky"); + invalidUser.setLastName("One"); + invalidUser.setEmail("funky@example.com"); + invalidUser.setActivated(true); + invalidUser.setImageUrl("http://placehold.it/50x50"); + invalidUser.setLangKey(Constants.DEFAULT_LANGUAGE); + invalidUser.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(invalidUser)).with(csrf())) + .andExpect(status().isBadRequest()); + + Optional user = userRepository.findOneByEmailIgnoreCase("funky@example.com"); + assertThat(user).isEmpty(); + } + + static Stream invalidUsers() { + return Stream.of( + createInvalidUser("bob", "password", "Bob", "Green", "invalid", true), // <-- invalid + createInvalidUser("bob", "123", "Bob", "Green", "bob@example.com", true), // password with only 3 digits + createInvalidUser("bob", null, "Bob", "Green", "bob@example.com", true) // invalid null password + ); + } + + @ParameterizedTest + @MethodSource("invalidUsers") + @Transactional + void testRegisterInvalidUsers(ManagedUserVM invalidUser) throws Exception { + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(invalidUser)).with(csrf())) + .andExpect(status().isBadRequest()); + + Optional user = userRepository.findOneByLogin("bob"); + assertThat(user).isEmpty(); + } + + private static ManagedUserVM createInvalidUser( + String login, + String password, + String firstName, + String lastName, + String email, + boolean activated + ) { + ManagedUserVM invalidUser = new ManagedUserVM(); + invalidUser.setLogin(login); + invalidUser.setPassword(password); + invalidUser.setFirstName(firstName); + invalidUser.setLastName(lastName); + invalidUser.setEmail(email); + invalidUser.setActivated(activated); + invalidUser.setImageUrl("http://placehold.it/50x50"); + invalidUser.setLangKey(Constants.DEFAULT_LANGUAGE); + invalidUser.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + return invalidUser; + } + + @Test + @Transactional + void testRegisterDuplicateLogin() throws Exception { + // First registration + ManagedUserVM firstUser = new ManagedUserVM(); + firstUser.setLogin("alice"); + firstUser.setPassword("password"); + firstUser.setFirstName("Alice"); + firstUser.setLastName("Something"); + firstUser.setEmail("alice@example.com"); + firstUser.setImageUrl("http://placehold.it/50x50"); + firstUser.setLangKey(Constants.DEFAULT_LANGUAGE); + firstUser.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + // Duplicate login, different email + ManagedUserVM secondUser = new ManagedUserVM(); + secondUser.setLogin(firstUser.getLogin()); + secondUser.setPassword(firstUser.getPassword()); + secondUser.setFirstName(firstUser.getFirstName()); + secondUser.setLastName(firstUser.getLastName()); + secondUser.setEmail("alice2@example.com"); + secondUser.setImageUrl(firstUser.getImageUrl()); + secondUser.setLangKey(firstUser.getLangKey()); + secondUser.setCreatedBy(firstUser.getCreatedBy()); + secondUser.setCreatedDate(firstUser.getCreatedDate()); + secondUser.setLastModifiedBy(firstUser.getLastModifiedBy()); + secondUser.setLastModifiedDate(firstUser.getLastModifiedDate()); + secondUser.setAuthorities(new HashSet<>(firstUser.getAuthorities())); + + // First user + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(firstUser)).with(csrf())) + .andExpect(status().isCreated()); + + // Second (non activated) user + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(secondUser)).with(csrf())) + .andExpect(status().isCreated()); + + Optional testUser = userRepository.findOneByEmailIgnoreCase("alice2@example.com"); + assertThat(testUser).isPresent(); + testUser.orElseThrow().setActivated(true); + userRepository.save(testUser.orElseThrow()); + + // Second (already activated) user + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(secondUser)).with(csrf())) + .andExpect(status().is4xxClientError()); + + userService.deleteUser("alice"); + } + + @Test + @Transactional + void testRegisterDuplicateEmail() throws Exception { + // First user + ManagedUserVM firstUser = new ManagedUserVM(); + firstUser.setLogin("test-register-duplicate-email"); + firstUser.setPassword("password"); + firstUser.setFirstName("Alice"); + firstUser.setLastName("Test"); + firstUser.setEmail("test-register-duplicate-email@example.com"); + firstUser.setImageUrl("http://placehold.it/50x50"); + firstUser.setLangKey(Constants.DEFAULT_LANGUAGE); + firstUser.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + // Register first user + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(firstUser)).with(csrf())) + .andExpect(status().isCreated()); + + Optional testUser1 = userRepository.findOneByLogin("test-register-duplicate-email"); + assertThat(testUser1).isPresent(); + + // Duplicate email, different login + ManagedUserVM secondUser = new ManagedUserVM(); + secondUser.setLogin("test-register-duplicate-email-2"); + secondUser.setPassword(firstUser.getPassword()); + secondUser.setFirstName(firstUser.getFirstName()); + secondUser.setLastName(firstUser.getLastName()); + secondUser.setEmail(firstUser.getEmail()); + secondUser.setImageUrl(firstUser.getImageUrl()); + secondUser.setLangKey(firstUser.getLangKey()); + secondUser.setAuthorities(new HashSet<>(firstUser.getAuthorities())); + + // Register second (non activated) user + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(secondUser)).with(csrf())) + .andExpect(status().isCreated()); + + Optional testUser2 = userRepository.findOneByLogin("test-register-duplicate-email"); + assertThat(testUser2).isEmpty(); + + Optional testUser3 = userRepository.findOneByLogin("test-register-duplicate-email-2"); + assertThat(testUser3).isPresent(); + + // Duplicate email - with uppercase email address + ManagedUserVM userWithUpperCaseEmail = new ManagedUserVM(); + userWithUpperCaseEmail.setId(firstUser.getId()); + userWithUpperCaseEmail.setLogin("test-register-duplicate-email-3"); + userWithUpperCaseEmail.setPassword(firstUser.getPassword()); + userWithUpperCaseEmail.setFirstName(firstUser.getFirstName()); + userWithUpperCaseEmail.setLastName(firstUser.getLastName()); + userWithUpperCaseEmail.setEmail("TEST-register-duplicate-email@example.com"); + userWithUpperCaseEmail.setImageUrl(firstUser.getImageUrl()); + userWithUpperCaseEmail.setLangKey(firstUser.getLangKey()); + userWithUpperCaseEmail.setAuthorities(new HashSet<>(firstUser.getAuthorities())); + + // Register third (not activated) user + restAccountMockMvc + .perform( + post("/api/register") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(userWithUpperCaseEmail)) + .with(csrf()) + ) + .andExpect(status().isCreated()); + + Optional testUser4 = userRepository.findOneByLogin("test-register-duplicate-email-3"); + assertThat(testUser4).isPresent(); + assertThat(testUser4.orElseThrow().getEmail()).isEqualTo("test-register-duplicate-email@example.com"); + + testUser4.orElseThrow().setActivated(true); + userService.updateUser((new AdminUserDTO(testUser4.orElseThrow()))); + + // Register 4th (already activated) user + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(secondUser)).with(csrf())) + .andExpect(status().is4xxClientError()); + + userService.deleteUser("test-register-duplicate-email-3"); + } + + @Test + @Transactional + void testRegisterAdminIsIgnored() throws Exception { + ManagedUserVM validUser = new ManagedUserVM(); + validUser.setLogin("badguy"); + validUser.setPassword("password"); + validUser.setFirstName("Bad"); + validUser.setLastName("Guy"); + validUser.setEmail("badguy@example.com"); + validUser.setActivated(true); + validUser.setImageUrl("http://placehold.it/50x50"); + validUser.setLangKey(Constants.DEFAULT_LANGUAGE); + validUser.setAuthorities(Collections.singleton(AuthoritiesConstants.ADMIN)); + + restAccountMockMvc + .perform(post("/api/register").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(validUser)).with(csrf())) + .andExpect(status().isCreated()); + + Optional userDup = userRepository.findOneWithAuthoritiesByLogin("badguy"); + assertThat(userDup).isPresent(); + assertThat(userDup.orElseThrow().getAuthorities()) + .hasSize(1) + .containsExactly(authorityRepository.findById(AuthoritiesConstants.USER).orElseThrow()); + + userService.deleteUser("badguy"); + } + + @Test + @Transactional + void testActivateAccount() throws Exception { + final String activationKey = "some activation key"; + User user = new User(); + user.setLogin("activate-account"); + user.setEmail("activate-account@example.com"); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(false); + user.setActivationKey(activationKey); + + userRepository.saveAndFlush(user); + + restAccountMockMvc.perform(get("/api/activate?key={activationKey}", activationKey)).andExpect(status().isOk()); + + user = userRepository.findOneByLogin(user.getLogin()).orElse(null); + assertThat(user.isActivated()).isTrue(); + + userService.deleteUser("activate-account"); + } + + @Test + @Transactional + void testActivateAccountWithWrongKey() throws Exception { + restAccountMockMvc.perform(get("/api/activate?key=wrongActivationKey")).andExpect(status().isInternalServerError()); + } + + @Test + @Transactional + @WithMockUser("save-account") + void testSaveAccount() throws Exception { + User user = new User(); + user.setLogin("save-account"); + user.setEmail("save-account@example.com"); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + userRepository.saveAndFlush(user); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setLogin("not-used"); + userDTO.setFirstName("firstname"); + userDTO.setLastName("lastname"); + userDTO.setEmail("save-account@example.com"); + userDTO.setActivated(false); + userDTO.setImageUrl("http://placehold.it/50x50"); + userDTO.setLangKey(Constants.DEFAULT_LANGUAGE); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.ADMIN)); + + restAccountMockMvc + .perform(post("/api/account").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isOk()); + + User updatedUser = userRepository.findOneWithAuthoritiesByLogin(user.getLogin()).orElse(null); + assertThat(updatedUser.getFirstName()).isEqualTo(userDTO.getFirstName()); + assertThat(updatedUser.getLastName()).isEqualTo(userDTO.getLastName()); + assertThat(updatedUser.getEmail()).isEqualTo(userDTO.getEmail()); + assertThat(updatedUser.getLangKey()).isEqualTo(userDTO.getLangKey()); + assertThat(updatedUser.getPassword()).isEqualTo(user.getPassword()); + assertThat(updatedUser.getImageUrl()).isEqualTo(userDTO.getImageUrl()); + assertThat(updatedUser.isActivated()).isTrue(); + assertThat(updatedUser.getAuthorities()).isEmpty(); + + userService.deleteUser("save-account"); + } + + @Test + @Transactional + @WithMockUser("save-invalid-email") + void testSaveInvalidEmail() throws Exception { + User user = new User(); + user.setLogin("save-invalid-email"); + user.setEmail("save-invalid-email@example.com"); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + + userRepository.saveAndFlush(user); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setLogin("not-used"); + userDTO.setFirstName("firstname"); + userDTO.setLastName("lastname"); + userDTO.setEmail("invalid email"); + userDTO.setActivated(false); + userDTO.setImageUrl("http://placehold.it/50x50"); + userDTO.setLangKey(Constants.DEFAULT_LANGUAGE); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.ADMIN)); + + restAccountMockMvc + .perform(post("/api/account").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + assertThat(userRepository.findOneByEmailIgnoreCase("invalid email")).isNotPresent(); + + userService.deleteUser("save-invalid-email"); + } + + @Test + @Transactional + @WithMockUser("save-existing-email") + void testSaveExistingEmail() throws Exception { + User user = new User(); + user.setLogin("save-existing-email"); + user.setEmail("save-existing-email@example.com"); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + userRepository.saveAndFlush(user); + + User anotherUser = new User(); + anotherUser.setLogin("save-existing-email2"); + anotherUser.setEmail("save-existing-email2@example.com"); + anotherUser.setPassword(RandomStringUtils.randomAlphanumeric(60)); + anotherUser.setActivated(true); + + userRepository.saveAndFlush(anotherUser); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setLogin("not-used"); + userDTO.setFirstName("firstname"); + userDTO.setLastName("lastname"); + userDTO.setEmail("save-existing-email2@example.com"); + userDTO.setActivated(false); + userDTO.setImageUrl("http://placehold.it/50x50"); + userDTO.setLangKey(Constants.DEFAULT_LANGUAGE); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.ADMIN)); + + restAccountMockMvc + .perform(post("/api/account").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + User updatedUser = userRepository.findOneByLogin("save-existing-email").orElseThrow(); + assertThat(updatedUser.getEmail()).isEqualTo("save-existing-email@example.com"); + + userService.deleteUser("save-existing-email"); + userService.deleteUser("save-existing-email2"); + } + + @Test + @Transactional + @WithMockUser("save-existing-email-and-login") + void testSaveExistingEmailAndLogin() throws Exception { + User user = new User(); + user.setLogin("save-existing-email-and-login"); + user.setEmail("save-existing-email-and-login@example.com"); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + userRepository.saveAndFlush(user); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setLogin("not-used"); + userDTO.setFirstName("firstname"); + userDTO.setLastName("lastname"); + userDTO.setEmail("save-existing-email-and-login@example.com"); + userDTO.setActivated(false); + userDTO.setImageUrl("http://placehold.it/50x50"); + userDTO.setLangKey(Constants.DEFAULT_LANGUAGE); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.ADMIN)); + + restAccountMockMvc + .perform(post("/api/account").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isOk()); + + User updatedUser = userRepository.findOneByLogin("save-existing-email-and-login").orElse(null); + assertThat(updatedUser.getEmail()).isEqualTo("save-existing-email-and-login@example.com"); + + userService.deleteUser("save-existing-email-and-login"); + } + + @Test + @Transactional + @WithMockUser("change-password-wrong-existing-password") + void testChangePasswordWrongExistingPassword() throws Exception { + User user = new User(); + String currentPassword = RandomStringUtils.randomAlphanumeric(60); + user.setPassword(passwordEncoder.encode(currentPassword)); + user.setLogin("change-password-wrong-existing-password"); + user.setEmail("change-password-wrong-existing-password@example.com"); + userRepository.saveAndFlush(user); + + restAccountMockMvc + .perform( + post("/api/account/change-password") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(new PasswordChangeDTO("1" + currentPassword, "new password"))) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + + User updatedUser = userRepository.findOneByLogin("change-password-wrong-existing-password").orElse(null); + assertThat(passwordEncoder.matches("new password", updatedUser.getPassword())).isFalse(); + assertThat(passwordEncoder.matches(currentPassword, updatedUser.getPassword())).isTrue(); + + userService.deleteUser("change-password-wrong-existing-password"); + } + + @Test + @Transactional + @WithMockUser("change-password") + void testChangePassword() throws Exception { + User user = new User(); + String currentPassword = RandomStringUtils.randomAlphanumeric(60); + user.setPassword(passwordEncoder.encode(currentPassword)); + user.setLogin("change-password"); + user.setEmail("change-password@example.com"); + userRepository.saveAndFlush(user); + + restAccountMockMvc + .perform( + post("/api/account/change-password") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(new PasswordChangeDTO(currentPassword, "new password"))) + .with(csrf()) + ) + .andExpect(status().isOk()); + + User updatedUser = userRepository.findOneByLogin("change-password").orElse(null); + assertThat(passwordEncoder.matches("new password", updatedUser.getPassword())).isTrue(); + + userService.deleteUser("change-password"); + } + + @Test + @Transactional + @WithMockUser("change-password-too-small") + void testChangePasswordTooSmall() throws Exception { + User user = new User(); + String currentPassword = RandomStringUtils.randomAlphanumeric(60); + user.setPassword(passwordEncoder.encode(currentPassword)); + user.setLogin("change-password-too-small"); + user.setEmail("change-password-too-small@example.com"); + userRepository.saveAndFlush(user); + + String newPassword = RandomStringUtils.random(ManagedUserVM.PASSWORD_MIN_LENGTH - 1); + + restAccountMockMvc + .perform( + post("/api/account/change-password") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(new PasswordChangeDTO(currentPassword, newPassword))) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + + User updatedUser = userRepository.findOneByLogin("change-password-too-small").orElse(null); + assertThat(updatedUser.getPassword()).isEqualTo(user.getPassword()); + + userService.deleteUser("change-password-too-small"); + } + + @Test + @Transactional + @WithMockUser("change-password-too-long") + void testChangePasswordTooLong() throws Exception { + User user = new User(); + String currentPassword = RandomStringUtils.randomAlphanumeric(60); + user.setPassword(passwordEncoder.encode(currentPassword)); + user.setLogin("change-password-too-long"); + user.setEmail("change-password-too-long@example.com"); + userRepository.saveAndFlush(user); + + String newPassword = RandomStringUtils.random(ManagedUserVM.PASSWORD_MAX_LENGTH + 1); + + restAccountMockMvc + .perform( + post("/api/account/change-password") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(new PasswordChangeDTO(currentPassword, newPassword))) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + + User updatedUser = userRepository.findOneByLogin("change-password-too-long").orElse(null); + assertThat(updatedUser.getPassword()).isEqualTo(user.getPassword()); + + userService.deleteUser("change-password-too-long"); + } + + @Test + @Transactional + @WithMockUser("change-password-empty") + void testChangePasswordEmpty() throws Exception { + User user = new User(); + String currentPassword = RandomStringUtils.randomAlphanumeric(60); + user.setPassword(passwordEncoder.encode(currentPassword)); + user.setLogin("change-password-empty"); + user.setEmail("change-password-empty@example.com"); + userRepository.saveAndFlush(user); + + restAccountMockMvc + .perform( + post("/api/account/change-password") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(new PasswordChangeDTO(currentPassword, ""))) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + + User updatedUser = userRepository.findOneByLogin("change-password-empty").orElse(null); + assertThat(updatedUser.getPassword()).isEqualTo(user.getPassword()); + + userService.deleteUser("change-password-empty"); + } + + @Test + @Transactional + @WithMockUser("current-sessions") + void testGetCurrentSessions() throws Exception { + User user = new User(); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setLogin("current-sessions"); + user.setEmail("current-sessions@example.com"); + userRepository.saveAndFlush(user); + + PersistentToken token = new PersistentToken(); + token.setSeries("current-sessions"); + token.setUser(user); + token.setTokenValue("current-session-data"); + token.setTokenDate(LocalDate.of(2017, 3, 23)); + + token.setIpAddress("127.0.0.1"); + token.setUserAgent("Test agent"); + persistentTokenRepository.saveAndFlush(token); + + restAccountMockMvc + .perform(get("/api/account/sessions")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[*].series").value(hasItem(token.getSeries()))) + .andExpect(jsonPath("$.[*].ipAddress").value(hasItem(token.getIpAddress()))) + .andExpect(jsonPath("$.[*].userAgent").value(hasItem(token.getUserAgent()))) + .andExpect(jsonPath("$.[*].tokenDate").value(hasItem(containsString(token.getTokenDate().toString())))); + + persistentTokenRepository.delete(token); + userService.deleteUser("current-sessions"); + } + + @Test + @Transactional + @WithMockUser("invalidate-session") + void testInvalidateSession() throws Exception { + User user = new User(); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setLogin("invalidate-session"); + user.setEmail("invalidate-session@example.com"); + userRepository.saveAndFlush(user); + + PersistentToken token = new PersistentToken(); + token.setSeries("invalidate-session"); + token.setUser(user); + token.setTokenValue("invalidate-data"); + token.setTokenDate(LocalDate.of(2017, 3, 23)); + token.setIpAddress("127.0.0.1"); + token.setUserAgent("Test agent"); + persistentTokenRepository.saveAndFlush(token); + + assertThat(persistentTokenRepository.findByUser(user)).hasSize(1); + + restAccountMockMvc.perform(delete("/api/account/sessions/invalidate-session").with(csrf())).andExpect(status().isOk()); + + assertThat(persistentTokenRepository.findByUser(user)).isEmpty(); + + persistentTokenRepository.delete(token); + userService.deleteUser("invalidate-session"); + } + + @Test + @Transactional + void testRequestPasswordReset() throws Exception { + User user = new User(); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + user.setLogin("password-reset"); + user.setEmail("password-reset@example.com"); + user.setLangKey("en"); + userRepository.saveAndFlush(user); + + restAccountMockMvc + .perform(post("/api/account/reset-password/init").content("password-reset@example.com").with(csrf())) + .andExpect(status().isOk()); + + userService.deleteUser("password-reset"); + } + + @Test + @Transactional + void testRequestPasswordResetUpperCaseEmail() throws Exception { + User user = new User(); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setActivated(true); + user.setLogin("password-reset-upper-case"); + user.setEmail("password-reset-upper-case@example.com"); + user.setLangKey("en"); + userRepository.saveAndFlush(user); + + restAccountMockMvc + .perform(post("/api/account/reset-password/init").content("password-reset-upper-case@EXAMPLE.COM").with(csrf())) + .andExpect(status().isOk()); + + userService.deleteUser("password-reset-upper-case"); + } + + @Test + void testRequestPasswordResetWrongEmail() throws Exception { + restAccountMockMvc + .perform(post("/api/account/reset-password/init").content("password-reset-wrong-email@example.com").with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @Transactional + void testFinishPasswordReset() throws Exception { + User user = new User(); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setLogin("finish-password-reset"); + user.setEmail("finish-password-reset@example.com"); + user.setResetDate(Instant.now().plusSeconds(60)); + user.setResetKey("reset key"); + userRepository.saveAndFlush(user); + + KeyAndPasswordVM keyAndPassword = new KeyAndPasswordVM(); + keyAndPassword.setKey(user.getResetKey()); + keyAndPassword.setNewPassword("new password"); + + restAccountMockMvc + .perform( + post("/api/account/reset-password/finish") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(keyAndPassword)) + .with(csrf()) + ) + .andExpect(status().isOk()); + + User updatedUser = userRepository.findOneByLogin(user.getLogin()).orElse(null); + assertThat(passwordEncoder.matches(keyAndPassword.getNewPassword(), updatedUser.getPassword())).isTrue(); + + userService.deleteUser("finish-password-reset"); + } + + @Test + @Transactional + void testFinishPasswordResetTooSmall() throws Exception { + User user = new User(); + user.setPassword(RandomStringUtils.randomAlphanumeric(60)); + user.setLogin("finish-password-reset-too-small"); + user.setEmail("finish-password-reset-too-small@example.com"); + user.setResetDate(Instant.now().plusSeconds(60)); + user.setResetKey("reset key too small"); + userRepository.saveAndFlush(user); + + KeyAndPasswordVM keyAndPassword = new KeyAndPasswordVM(); + keyAndPassword.setKey(user.getResetKey()); + keyAndPassword.setNewPassword("foo"); + + restAccountMockMvc + .perform( + post("/api/account/reset-password/finish") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(keyAndPassword)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()); + + User updatedUser = userRepository.findOneByLogin(user.getLogin()).orElse(null); + assertThat(passwordEncoder.matches(keyAndPassword.getNewPassword(), updatedUser.getPassword())).isFalse(); + + userService.deleteUser("finish-password-reset-too-small"); + } + + @Test + @Transactional + void testFinishPasswordResetWrongKey() throws Exception { + KeyAndPasswordVM keyAndPassword = new KeyAndPasswordVM(); + keyAndPassword.setKey("wrong reset key"); + keyAndPassword.setNewPassword("new password"); + + restAccountMockMvc + .perform( + post("/api/account/reset-password/finish") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(keyAndPassword)) + .with(csrf()) + ) + .andExpect(status().isInternalServerError()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/AuthorityResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/AuthorityResourceIT.java new file mode 100644 index 0000000..63f9498 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/AuthorityResourceIT.java @@ -0,0 +1,209 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.AuthorityAsserts.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.Authority; +import com.oguerreiro.resilient.repository.AuthorityRepository; +import jakarta.persistence.EntityManager; +import java.util.UUID; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link AuthorityResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +@WithMockUser(authorities = { "ROLE_ADMIN" }) +class AuthorityResourceIT { + + private static final String ENTITY_API_URL = "/api/authorities"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{name}"; + + @Autowired + private ObjectMapper om; + + @Autowired + private AuthorityRepository authorityRepository; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restAuthorityMockMvc; + + private Authority authority; + + private Authority insertedAuthority; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Authority createEntity(EntityManager em) { + Authority authority = new Authority().name(UUID.randomUUID().toString()); + return authority; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Authority createUpdatedEntity(EntityManager em) { + Authority authority = new Authority().name(UUID.randomUUID().toString()); + return authority; + } + + @BeforeEach + public void initTest() { + authority = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedAuthority != null) { + authorityRepository.delete(insertedAuthority); + insertedAuthority = null; + } + } + + @Test + @Transactional + void createAuthority() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the Authority + var returnedAuthority = om.readValue( + restAuthorityMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(authority))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + Authority.class + ); + + // Validate the Authority in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + assertAuthorityUpdatableFieldsEquals(returnedAuthority, getPersistedAuthority(returnedAuthority)); + + insertedAuthority = returnedAuthority; + } + + @Test + @Transactional + void createAuthorityWithExistingId() throws Exception { + // Create the Authority with an existing ID + insertedAuthority = authorityRepository.saveAndFlush(authority); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restAuthorityMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(authority))) + .andExpect(status().isBadRequest()); + + // Validate the Authority in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void getAllAuthorities() throws Exception { + // Initialize the database + authority.setName(UUID.randomUUID().toString()); + insertedAuthority = authorityRepository.saveAndFlush(authority); + + // Get all the authorityList + restAuthorityMockMvc + .perform(get(ENTITY_API_URL + "?sort=name,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].name").value(hasItem(authority.getName()))); + } + + @Test + @Transactional + void getAuthority() throws Exception { + // Initialize the database + authority.setName(UUID.randomUUID().toString()); + insertedAuthority = authorityRepository.saveAndFlush(authority); + + // Get the authority + restAuthorityMockMvc + .perform(get(ENTITY_API_URL_ID, authority.getName())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.name").value(authority.getName())); + } + + @Test + @Transactional + void getNonExistingAuthority() throws Exception { + // Get the authority + restAuthorityMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void deleteAuthority() throws Exception { + // Initialize the database + authority.setName(UUID.randomUUID().toString()); + insertedAuthority = authorityRepository.saveAndFlush(authority); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the authority + restAuthorityMockMvc + .perform(delete(ENTITY_API_URL_ID, authority.getName()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return authorityRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected Authority getPersistedAuthority(Authority authority) { + return authorityRepository.findById(authority.getName()).orElseThrow(); + } + + protected void assertPersistedAuthorityToMatchAllProperties(Authority expectedAuthority) { + assertAuthorityAllPropertiesEquals(expectedAuthority, getPersistedAuthority(expectedAuthority)); + } + + protected void assertPersistedAuthorityToMatchUpdatableProperties(Authority expectedAuthority) { + assertAuthorityAllUpdatablePropertiesEquals(expectedAuthority, getPersistedAuthority(expectedAuthority)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/InputDataResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/InputDataResourceIT.java new file mode 100644 index 0000000..dabb610 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/InputDataResourceIT.java @@ -0,0 +1,739 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.InputDataAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static com.oguerreiro.resilient.web.rest.TestUtil.sameNumber; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.InputData; +import com.oguerreiro.resilient.domain.enumeration.DataSourceType; +import com.oguerreiro.resilient.repository.InputDataRepository; +import com.oguerreiro.resilient.service.InputDataService; +import com.oguerreiro.resilient.service.dto.InputDataDTO; +import com.oguerreiro.resilient.service.mapper.InputDataMapper; +import jakarta.persistence.EntityManager; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link InputDataResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class InputDataResourceIT { + + private static final BigDecimal DEFAULT_SOURCE_VALUE = new BigDecimal(1); + private static final BigDecimal UPDATED_SOURCE_VALUE = new BigDecimal(2); + + private static final BigDecimal DEFAULT_VARIABLE_VALUE = new BigDecimal(1); + private static final BigDecimal UPDATED_VARIABLE_VALUE = new BigDecimal(2); + + private static final BigDecimal DEFAULT_IMPUTED_VALUE = new BigDecimal(1); + private static final BigDecimal UPDATED_IMPUTED_VALUE = new BigDecimal(2); + + private static final DataSourceType DEFAULT_SOURCE_TYPE = DataSourceType.MANUAL; + private static final DataSourceType UPDATED_SOURCE_TYPE = DataSourceType.FILE; + + private static final LocalDate DEFAULT_DATA_DATE = LocalDate.ofEpochDay(0L); + private static final LocalDate UPDATED_DATA_DATE = LocalDate.now(ZoneId.systemDefault()); + + private static final Instant DEFAULT_CHANGE_DATE = Instant.ofEpochMilli(0L); + private static final Instant UPDATED_CHANGE_DATE = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + private static final String DEFAULT_CHANGE_USERNAME = "AAAAAAAAAA"; + private static final String UPDATED_CHANGE_USERNAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DATA_SOURCE = "AAAAAAAAAA"; + private static final String UPDATED_DATA_SOURCE = "BBBBBBBBBB"; + + private static final String DEFAULT_DATA_USER = "AAAAAAAAAA"; + private static final String UPDATED_DATA_USER = "BBBBBBBBBB"; + + private static final String DEFAULT_DATA_COMMENTS = "AAAAAAAAAA"; + private static final String UPDATED_DATA_COMMENTS = "BBBBBBBBBB"; + + private static final Instant DEFAULT_CREATION_DATE = Instant.ofEpochMilli(0L); + private static final Instant UPDATED_CREATION_DATE = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + private static final String DEFAULT_CREATION_USERNAME = "AAAAAAAAAA"; + private static final String UPDATED_CREATION_USERNAME = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/input-data"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private InputDataRepository inputDataRepository; + + @Mock + private InputDataRepository inputDataRepositoryMock; + + @Autowired + private InputDataMapper inputDataMapper; + + @Mock + private InputDataService inputDataServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restInputDataMockMvc; + + private InputData inputData; + + private InputData insertedInputData; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static InputData createEntity(EntityManager em) { + InputData inputData = new InputData() + .sourceValue(DEFAULT_SOURCE_VALUE) + .variableValue(DEFAULT_VARIABLE_VALUE) + .imputedValue(DEFAULT_IMPUTED_VALUE) + .sourceType(DEFAULT_SOURCE_TYPE) + .dataDate(DEFAULT_DATA_DATE) + .changeDate(DEFAULT_CHANGE_DATE) + .changeUsername(DEFAULT_CHANGE_USERNAME) + .dataSource(DEFAULT_DATA_SOURCE) + .dataUser(DEFAULT_DATA_USER) + .dataComments(DEFAULT_DATA_COMMENTS) + .creationDate(DEFAULT_CREATION_DATE) + .creationUsername(DEFAULT_CREATION_USERNAME) + .version(DEFAULT_VERSION); + return inputData; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static InputData createUpdatedEntity(EntityManager em) { + InputData inputData = new InputData() + .sourceValue(UPDATED_SOURCE_VALUE) + .variableValue(UPDATED_VARIABLE_VALUE) + .imputedValue(UPDATED_IMPUTED_VALUE) + .sourceType(UPDATED_SOURCE_TYPE) + .dataDate(UPDATED_DATA_DATE) + .changeDate(UPDATED_CHANGE_DATE) + .changeUsername(UPDATED_CHANGE_USERNAME) + .dataSource(UPDATED_DATA_SOURCE) + .dataUser(UPDATED_DATA_USER) + .dataComments(UPDATED_DATA_COMMENTS) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + return inputData; + } + + @BeforeEach + public void initTest() { + inputData = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedInputData != null) { + inputDataRepository.delete(insertedInputData); + insertedInputData = null; + } + } + + @Test + @Transactional + void createInputData() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the InputData + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + var returnedInputDataDTO = om.readValue( + restInputDataMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + InputDataDTO.class + ); + + // Validate the InputData in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedInputData = inputDataMapper.toEntity(returnedInputDataDTO); + assertInputDataUpdatableFieldsEquals(returnedInputData, getPersistedInputData(returnedInputData)); + + insertedInputData = returnedInputData; + } + + @Test + @Transactional + void createInputDataWithExistingId() throws Exception { + // Create the InputData with an existing ID + inputData.setId(1L); + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkSourceValueIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setSourceValue(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkVariableValueIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setVariableValue(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkImputedValueIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setImputedValue(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkSourceTypeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setSourceType(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkChangeDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setChangeDate(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkChangeUsernameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setChangeUsername(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setCreationDate(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationUsernameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputData.setCreationUsername(null); + + // Create the InputData, which fails. + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + restInputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllInputData() throws Exception { + // Initialize the database + insertedInputData = inputDataRepository.saveAndFlush(inputData); + + // Get all the inputDataList + restInputDataMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(inputData.getId().intValue()))) + .andExpect(jsonPath("$.[*].sourceValue").value(hasItem(sameNumber(DEFAULT_SOURCE_VALUE)))) + .andExpect(jsonPath("$.[*].variableValue").value(hasItem(sameNumber(DEFAULT_VARIABLE_VALUE)))) + .andExpect(jsonPath("$.[*].imputedValue").value(hasItem(sameNumber(DEFAULT_IMPUTED_VALUE)))) + .andExpect(jsonPath("$.[*].sourceType").value(hasItem(DEFAULT_SOURCE_TYPE.toString()))) + .andExpect(jsonPath("$.[*].dataDate").value(hasItem(DEFAULT_DATA_DATE.toString()))) + .andExpect(jsonPath("$.[*].changeDate").value(hasItem(DEFAULT_CHANGE_DATE.toString()))) + .andExpect(jsonPath("$.[*].changeUsername").value(hasItem(DEFAULT_CHANGE_USERNAME))) + .andExpect(jsonPath("$.[*].dataSource").value(hasItem(DEFAULT_DATA_SOURCE))) + .andExpect(jsonPath("$.[*].dataUser").value(hasItem(DEFAULT_DATA_USER))) + .andExpect(jsonPath("$.[*].dataComments").value(hasItem(DEFAULT_DATA_COMMENTS))) + .andExpect(jsonPath("$.[*].creationDate").value(hasItem(DEFAULT_CREATION_DATE.toString()))) + .andExpect(jsonPath("$.[*].creationUsername").value(hasItem(DEFAULT_CREATION_USERNAME))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllInputDataWithEagerRelationshipsIsEnabled() throws Exception { + when(inputDataServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restInputDataMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(inputDataServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllInputDataWithEagerRelationshipsIsNotEnabled() throws Exception { + when(inputDataServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restInputDataMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(inputDataRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getInputData() throws Exception { + // Initialize the database + insertedInputData = inputDataRepository.saveAndFlush(inputData); + + // Get the inputData + restInputDataMockMvc + .perform(get(ENTITY_API_URL_ID, inputData.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(inputData.getId().intValue())) + .andExpect(jsonPath("$.sourceValue").value(sameNumber(DEFAULT_SOURCE_VALUE))) + .andExpect(jsonPath("$.variableValue").value(sameNumber(DEFAULT_VARIABLE_VALUE))) + .andExpect(jsonPath("$.imputedValue").value(sameNumber(DEFAULT_IMPUTED_VALUE))) + .andExpect(jsonPath("$.sourceType").value(DEFAULT_SOURCE_TYPE.toString())) + .andExpect(jsonPath("$.dataDate").value(DEFAULT_DATA_DATE.toString())) + .andExpect(jsonPath("$.changeDate").value(DEFAULT_CHANGE_DATE.toString())) + .andExpect(jsonPath("$.changeUsername").value(DEFAULT_CHANGE_USERNAME)) + .andExpect(jsonPath("$.dataSource").value(DEFAULT_DATA_SOURCE)) + .andExpect(jsonPath("$.dataUser").value(DEFAULT_DATA_USER)) + .andExpect(jsonPath("$.dataComments").value(DEFAULT_DATA_COMMENTS)) + .andExpect(jsonPath("$.creationDate").value(DEFAULT_CREATION_DATE.toString())) + .andExpect(jsonPath("$.creationUsername").value(DEFAULT_CREATION_USERNAME)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingInputData() throws Exception { + // Get the inputData + restInputDataMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingInputData() throws Exception { + // Initialize the database + insertedInputData = inputDataRepository.saveAndFlush(inputData); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputData + InputData updatedInputData = inputDataRepository.findById(inputData.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedInputData are not directly saved in db + em.detach(updatedInputData); + updatedInputData + .sourceValue(UPDATED_SOURCE_VALUE) + .variableValue(UPDATED_VARIABLE_VALUE) + .imputedValue(UPDATED_IMPUTED_VALUE) + .sourceType(UPDATED_SOURCE_TYPE) + .dataDate(UPDATED_DATA_DATE) + .changeDate(UPDATED_CHANGE_DATE) + .changeUsername(UPDATED_CHANGE_USERNAME) + .dataSource(UPDATED_DATA_SOURCE) + .dataUser(UPDATED_DATA_USER) + .dataComments(UPDATED_DATA_COMMENTS) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + InputDataDTO inputDataDTO = inputDataMapper.toDto(updatedInputData); + + restInputDataMockMvc + .perform( + put(ENTITY_API_URL_ID, inputDataDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataDTO)) + ) + .andExpect(status().isOk()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedInputDataToMatchAllProperties(updatedInputData); + } + + @Test + @Transactional + void putNonExistingInputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputData.setId(longCount.incrementAndGet()); + + // Create the InputData + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restInputDataMockMvc + .perform( + put(ENTITY_API_URL_ID, inputDataDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchInputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputData.setId(longCount.incrementAndGet()); + + // Create the InputData + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamInputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputData.setId(longCount.incrementAndGet()); + + // Create the InputData + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataMockMvc + .perform(put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataDTO))) + .andExpect(status().isMethodNotAllowed()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateInputDataWithPatch() throws Exception { + // Initialize the database + insertedInputData = inputDataRepository.saveAndFlush(inputData); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputData using partial update + InputData partialUpdatedInputData = new InputData(); + partialUpdatedInputData.setId(inputData.getId()); + + partialUpdatedInputData + .imputedValue(UPDATED_IMPUTED_VALUE) + .changeUsername(UPDATED_CHANGE_USERNAME) + .dataSource(UPDATED_DATA_SOURCE) + .dataUser(UPDATED_DATA_USER) + .dataComments(UPDATED_DATA_COMMENTS) + .creationUsername(UPDATED_CREATION_USERNAME); + + restInputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedInputData.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedInputData)) + ) + .andExpect(status().isOk()); + + // Validate the InputData in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertInputDataUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedInputData, inputData), + getPersistedInputData(inputData) + ); + } + + @Test + @Transactional + void fullUpdateInputDataWithPatch() throws Exception { + // Initialize the database + insertedInputData = inputDataRepository.saveAndFlush(inputData); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputData using partial update + InputData partialUpdatedInputData = new InputData(); + partialUpdatedInputData.setId(inputData.getId()); + + partialUpdatedInputData + .sourceValue(UPDATED_SOURCE_VALUE) + .variableValue(UPDATED_VARIABLE_VALUE) + .imputedValue(UPDATED_IMPUTED_VALUE) + .sourceType(UPDATED_SOURCE_TYPE) + .dataDate(UPDATED_DATA_DATE) + .changeDate(UPDATED_CHANGE_DATE) + .changeUsername(UPDATED_CHANGE_USERNAME) + .dataSource(UPDATED_DATA_SOURCE) + .dataUser(UPDATED_DATA_USER) + .dataComments(UPDATED_DATA_COMMENTS) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + + restInputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedInputData.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedInputData)) + ) + .andExpect(status().isOk()); + + // Validate the InputData in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertInputDataUpdatableFieldsEquals(partialUpdatedInputData, getPersistedInputData(partialUpdatedInputData)); + } + + @Test + @Transactional + void patchNonExistingInputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputData.setId(longCount.incrementAndGet()); + + // Create the InputData + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restInputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, inputDataDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchInputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputData.setId(longCount.incrementAndGet()); + + // Create the InputData + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamInputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputData.setId(longCount.incrementAndGet()); + + // Create the InputData + InputDataDTO inputDataDTO = inputDataMapper.toDto(inputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataMockMvc + .perform( + patch(ENTITY_API_URL).with(csrf()).contentType("application/merge-patch+json").content(om.writeValueAsBytes(inputDataDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the InputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteInputData() throws Exception { + // Initialize the database + insertedInputData = inputDataRepository.saveAndFlush(inputData); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the inputData + restInputDataMockMvc + .perform(delete(ENTITY_API_URL_ID, inputData.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return inputDataRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected InputData getPersistedInputData(InputData inputData) { + return inputDataRepository.findById(inputData.getId()).orElseThrow(); + } + + protected void assertPersistedInputDataToMatchAllProperties(InputData expectedInputData) { + assertInputDataAllPropertiesEquals(expectedInputData, getPersistedInputData(expectedInputData)); + } + + protected void assertPersistedInputDataToMatchUpdatableProperties(InputData expectedInputData) { + assertInputDataAllUpdatablePropertiesEquals(expectedInputData, getPersistedInputData(expectedInputData)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/InputDataUploadLogResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/InputDataUploadLogResourceIT.java new file mode 100644 index 0000000..91adb58 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/InputDataUploadLogResourceIT.java @@ -0,0 +1,602 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.InputDataUploadLogAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.InputDataUploadLog; +import com.oguerreiro.resilient.repository.InputDataUploadLogRepository; +import com.oguerreiro.resilient.service.InputDataUploadLogService; +import com.oguerreiro.resilient.service.dto.InputDataUploadLogDTO; +import com.oguerreiro.resilient.service.mapper.InputDataUploadLogMapper; +import jakarta.persistence.EntityManager; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link InputDataUploadLogResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class InputDataUploadLogResourceIT { + + private static final String DEFAULT_LOG_MESSAGE = "AAAAAAAAAA"; + private static final String UPDATED_LOG_MESSAGE = "BBBBBBBBBB"; + + private static final Instant DEFAULT_CREATION_DATE = Instant.ofEpochMilli(0L); + private static final Instant UPDATED_CREATION_DATE = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + private static final String DEFAULT_CREATION_USERNAME = "AAAAAAAAAA"; + private static final String UPDATED_CREATION_USERNAME = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/input-data-upload-logs"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private InputDataUploadLogRepository inputDataUploadLogRepository; + + @Mock + private InputDataUploadLogRepository inputDataUploadLogRepositoryMock; + + @Autowired + private InputDataUploadLogMapper inputDataUploadLogMapper; + + @Mock + private InputDataUploadLogService inputDataUploadLogServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restInputDataUploadLogMockMvc; + + private InputDataUploadLog inputDataUploadLog; + + private InputDataUploadLog insertedInputDataUploadLog; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static InputDataUploadLog createEntity(EntityManager em) { + InputDataUploadLog inputDataUploadLog = new InputDataUploadLog() + .logMessage(DEFAULT_LOG_MESSAGE) + .creationDate(DEFAULT_CREATION_DATE) + .creationUsername(DEFAULT_CREATION_USERNAME) + .version(DEFAULT_VERSION); + return inputDataUploadLog; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static InputDataUploadLog createUpdatedEntity(EntityManager em) { + InputDataUploadLog inputDataUploadLog = new InputDataUploadLog() + .logMessage(UPDATED_LOG_MESSAGE) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + return inputDataUploadLog; + } + + @BeforeEach + public void initTest() { + inputDataUploadLog = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedInputDataUploadLog != null) { + inputDataUploadLogRepository.delete(insertedInputDataUploadLog); + insertedInputDataUploadLog = null; + } + } + + @Test + @Transactional + void createInputDataUploadLog() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the InputDataUploadLog + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + var returnedInputDataUploadLogDTO = om.readValue( + restInputDataUploadLogMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + InputDataUploadLogDTO.class + ); + + // Validate the InputDataUploadLog in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedInputDataUploadLog = inputDataUploadLogMapper.toEntity(returnedInputDataUploadLogDTO); + assertInputDataUploadLogUpdatableFieldsEquals( + returnedInputDataUploadLog, + getPersistedInputDataUploadLog(returnedInputDataUploadLog) + ); + + insertedInputDataUploadLog = returnedInputDataUploadLog; + } + + @Test + @Transactional + void createInputDataUploadLogWithExistingId() throws Exception { + // Create the InputDataUploadLog with an existing ID + inputDataUploadLog.setId(1L); + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restInputDataUploadLogMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkLogMessageIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUploadLog.setLogMessage(null); + + // Create the InputDataUploadLog, which fails. + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + restInputDataUploadLogMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUploadLog.setCreationDate(null); + + // Create the InputDataUploadLog, which fails. + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + restInputDataUploadLogMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationUsernameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUploadLog.setCreationUsername(null); + + // Create the InputDataUploadLog, which fails. + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + restInputDataUploadLogMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllInputDataUploadLogs() throws Exception { + // Initialize the database + insertedInputDataUploadLog = inputDataUploadLogRepository.saveAndFlush(inputDataUploadLog); + + // Get all the inputDataUploadLogList + restInputDataUploadLogMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(inputDataUploadLog.getId().intValue()))) + .andExpect(jsonPath("$.[*].logMessage").value(hasItem(DEFAULT_LOG_MESSAGE))) + .andExpect(jsonPath("$.[*].creationDate").value(hasItem(DEFAULT_CREATION_DATE.toString()))) + .andExpect(jsonPath("$.[*].creationUsername").value(hasItem(DEFAULT_CREATION_USERNAME))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllInputDataUploadLogsWithEagerRelationshipsIsEnabled() throws Exception { + when(inputDataUploadLogServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restInputDataUploadLogMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(inputDataUploadLogServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllInputDataUploadLogsWithEagerRelationshipsIsNotEnabled() throws Exception { + when(inputDataUploadLogServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restInputDataUploadLogMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(inputDataUploadLogRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getInputDataUploadLog() throws Exception { + // Initialize the database + insertedInputDataUploadLog = inputDataUploadLogRepository.saveAndFlush(inputDataUploadLog); + + // Get the inputDataUploadLog + restInputDataUploadLogMockMvc + .perform(get(ENTITY_API_URL_ID, inputDataUploadLog.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(inputDataUploadLog.getId().intValue())) + .andExpect(jsonPath("$.logMessage").value(DEFAULT_LOG_MESSAGE)) + .andExpect(jsonPath("$.creationDate").value(DEFAULT_CREATION_DATE.toString())) + .andExpect(jsonPath("$.creationUsername").value(DEFAULT_CREATION_USERNAME)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingInputDataUploadLog() throws Exception { + // Get the inputDataUploadLog + restInputDataUploadLogMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingInputDataUploadLog() throws Exception { + // Initialize the database + insertedInputDataUploadLog = inputDataUploadLogRepository.saveAndFlush(inputDataUploadLog); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputDataUploadLog + InputDataUploadLog updatedInputDataUploadLog = inputDataUploadLogRepository.findById(inputDataUploadLog.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedInputDataUploadLog are not directly saved in db + em.detach(updatedInputDataUploadLog); + updatedInputDataUploadLog + .logMessage(UPDATED_LOG_MESSAGE) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(updatedInputDataUploadLog); + + restInputDataUploadLogMockMvc + .perform( + put(ENTITY_API_URL_ID, inputDataUploadLogDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isOk()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedInputDataUploadLogToMatchAllProperties(updatedInputDataUploadLog); + } + + @Test + @Transactional + void putNonExistingInputDataUploadLog() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUploadLog.setId(longCount.incrementAndGet()); + + // Create the InputDataUploadLog + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restInputDataUploadLogMockMvc + .perform( + put(ENTITY_API_URL_ID, inputDataUploadLogDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchInputDataUploadLog() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUploadLog.setId(longCount.incrementAndGet()); + + // Create the InputDataUploadLog + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadLogMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamInputDataUploadLog() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUploadLog.setId(longCount.incrementAndGet()); + + // Create the InputDataUploadLog + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadLogMockMvc + .perform( + put(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateInputDataUploadLogWithPatch() throws Exception { + // Initialize the database + insertedInputDataUploadLog = inputDataUploadLogRepository.saveAndFlush(inputDataUploadLog); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputDataUploadLog using partial update + InputDataUploadLog partialUpdatedInputDataUploadLog = new InputDataUploadLog(); + partialUpdatedInputDataUploadLog.setId(inputDataUploadLog.getId()); + + partialUpdatedInputDataUploadLog.logMessage(UPDATED_LOG_MESSAGE).creationDate(UPDATED_CREATION_DATE); + + restInputDataUploadLogMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedInputDataUploadLog.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedInputDataUploadLog)) + ) + .andExpect(status().isOk()); + + // Validate the InputDataUploadLog in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertInputDataUploadLogUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedInputDataUploadLog, inputDataUploadLog), + getPersistedInputDataUploadLog(inputDataUploadLog) + ); + } + + @Test + @Transactional + void fullUpdateInputDataUploadLogWithPatch() throws Exception { + // Initialize the database + insertedInputDataUploadLog = inputDataUploadLogRepository.saveAndFlush(inputDataUploadLog); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputDataUploadLog using partial update + InputDataUploadLog partialUpdatedInputDataUploadLog = new InputDataUploadLog(); + partialUpdatedInputDataUploadLog.setId(inputDataUploadLog.getId()); + + partialUpdatedInputDataUploadLog + .logMessage(UPDATED_LOG_MESSAGE) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + + restInputDataUploadLogMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedInputDataUploadLog.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedInputDataUploadLog)) + ) + .andExpect(status().isOk()); + + // Validate the InputDataUploadLog in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertInputDataUploadLogUpdatableFieldsEquals( + partialUpdatedInputDataUploadLog, + getPersistedInputDataUploadLog(partialUpdatedInputDataUploadLog) + ); + } + + @Test + @Transactional + void patchNonExistingInputDataUploadLog() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUploadLog.setId(longCount.incrementAndGet()); + + // Create the InputDataUploadLog + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restInputDataUploadLogMockMvc + .perform( + patch(ENTITY_API_URL_ID, inputDataUploadLogDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchInputDataUploadLog() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUploadLog.setId(longCount.incrementAndGet()); + + // Create the InputDataUploadLog + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadLogMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamInputDataUploadLog() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUploadLog.setId(longCount.incrementAndGet()); + + // Create the InputDataUploadLog + InputDataUploadLogDTO inputDataUploadLogDTO = inputDataUploadLogMapper.toDto(inputDataUploadLog); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadLogMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataUploadLogDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the InputDataUploadLog in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteInputDataUploadLog() throws Exception { + // Initialize the database + insertedInputDataUploadLog = inputDataUploadLogRepository.saveAndFlush(inputDataUploadLog); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the inputDataUploadLog + restInputDataUploadLogMockMvc + .perform(delete(ENTITY_API_URL_ID, inputDataUploadLog.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return inputDataUploadLogRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected InputDataUploadLog getPersistedInputDataUploadLog(InputDataUploadLog inputDataUploadLog) { + return inputDataUploadLogRepository.findById(inputDataUploadLog.getId()).orElseThrow(); + } + + protected void assertPersistedInputDataUploadLogToMatchAllProperties(InputDataUploadLog expectedInputDataUploadLog) { + assertInputDataUploadLogAllPropertiesEquals(expectedInputDataUploadLog, getPersistedInputDataUploadLog(expectedInputDataUploadLog)); + } + + protected void assertPersistedInputDataUploadLogToMatchUpdatableProperties(InputDataUploadLog expectedInputDataUploadLog) { + assertInputDataUploadLogAllUpdatablePropertiesEquals( + expectedInputDataUploadLog, + getPersistedInputDataUploadLog(expectedInputDataUploadLog) + ); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/InputDataUploadResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/InputDataUploadResourceIT.java new file mode 100644 index 0000000..79af02a --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/InputDataUploadResourceIT.java @@ -0,0 +1,689 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.InputDataUploadAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.InputDataUpload; +import com.oguerreiro.resilient.domain.enumeration.UploadStatus; +import com.oguerreiro.resilient.domain.enumeration.UploadType; +import com.oguerreiro.resilient.repository.InputDataUploadRepository; +import com.oguerreiro.resilient.service.InputDataUploadService; +import com.oguerreiro.resilient.service.dto.InputDataUploadDTO; +import com.oguerreiro.resilient.service.mapper.InputDataUploadMapper; +import jakarta.persistence.EntityManager; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link InputDataUploadResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class InputDataUploadResourceIT { + + private static final String DEFAULT_TITLE = "AAAAAAAAAA"; + private static final String UPDATED_TITLE = "BBBBBBBBBB"; + + private static final byte[] DEFAULT_DATA_FILE = TestUtil.createByteArray(1, "0"); + private static final byte[] UPDATED_DATA_FILE = TestUtil.createByteArray(1, "1"); + private static final String DEFAULT_DATA_FILE_CONTENT_TYPE = "image/jpg"; + private static final String UPDATED_DATA_FILE_CONTENT_TYPE = "image/png"; + + private static final String DEFAULT_UPLOAD_FILE_NAME = "AAAAAAAAAA"; + private static final String UPDATED_UPLOAD_FILE_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DISK_FILE_NAME = "AAAAAAAAAA"; + private static final String UPDATED_DISK_FILE_NAME = "BBBBBBBBBB"; + + private static final UploadType DEFAULT_TYPE = UploadType.INVENTORY; + private static final UploadType UPDATED_TYPE = UploadType.ACCOUNTING; + + private static final UploadStatus DEFAULT_STATE = UploadStatus.UPLOADED; + private static final UploadStatus UPDATED_STATE = UploadStatus.PROCESSING; + + private static final String DEFAULT_COMMENTS = "AAAAAAAAAA"; + private static final String UPDATED_COMMENTS = "BBBBBBBBBB"; + + private static final Instant DEFAULT_CREATION_DATE = Instant.ofEpochMilli(0L); + private static final Instant UPDATED_CREATION_DATE = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + private static final String DEFAULT_CREATION_USERNAME = "AAAAAAAAAA"; + private static final String UPDATED_CREATION_USERNAME = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/input-data-uploads"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private InputDataUploadRepository inputDataUploadRepository; + + @Mock + private InputDataUploadRepository inputDataUploadRepositoryMock; + + @Autowired + private InputDataUploadMapper inputDataUploadMapper; + + @Mock + private InputDataUploadService inputDataUploadServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restInputDataUploadMockMvc; + + private InputDataUpload inputDataUpload; + + private InputDataUpload insertedInputDataUpload; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static InputDataUpload createEntity(EntityManager em) { + InputDataUpload inputDataUpload = new InputDataUpload() + .title(DEFAULT_TITLE) + .dataFile(DEFAULT_DATA_FILE) + .dataFileContentType(DEFAULT_DATA_FILE_CONTENT_TYPE) + .uploadFileName(DEFAULT_UPLOAD_FILE_NAME) + .diskFileName(DEFAULT_DISK_FILE_NAME) + .type(DEFAULT_TYPE) + .state(DEFAULT_STATE) + .comments(DEFAULT_COMMENTS) + .creationDate(DEFAULT_CREATION_DATE) + .creationUsername(DEFAULT_CREATION_USERNAME) + .version(DEFAULT_VERSION); + return inputDataUpload; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static InputDataUpload createUpdatedEntity(EntityManager em) { + InputDataUpload inputDataUpload = new InputDataUpload() + .title(UPDATED_TITLE) + .dataFile(UPDATED_DATA_FILE) + .dataFileContentType(UPDATED_DATA_FILE_CONTENT_TYPE) + .uploadFileName(UPDATED_UPLOAD_FILE_NAME) + .diskFileName(UPDATED_DISK_FILE_NAME) + .type(UPDATED_TYPE) + .state(UPDATED_STATE) + .comments(UPDATED_COMMENTS) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + return inputDataUpload; + } + + @BeforeEach + public void initTest() { + inputDataUpload = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedInputDataUpload != null) { + inputDataUploadRepository.delete(insertedInputDataUpload); + insertedInputDataUpload = null; + } + } + + @Test + @Transactional + void createInputDataUpload() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the InputDataUpload + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + var returnedInputDataUploadDTO = om.readValue( + restInputDataUploadMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + InputDataUploadDTO.class + ); + + // Validate the InputDataUpload in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedInputDataUpload = inputDataUploadMapper.toEntity(returnedInputDataUploadDTO); + assertInputDataUploadUpdatableFieldsEquals(returnedInputDataUpload, getPersistedInputDataUpload(returnedInputDataUpload)); + + insertedInputDataUpload = returnedInputDataUpload; + } + + @Test + @Transactional + void createInputDataUploadWithExistingId() throws Exception { + // Create the InputDataUpload with an existing ID + inputDataUpload.setId(1L); + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restInputDataUploadMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkTitleIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUpload.setTitle(null); + + // Create the InputDataUpload, which fails. + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + restInputDataUploadMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkTypeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUpload.setType(null); + + // Create the InputDataUpload, which fails. + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + restInputDataUploadMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkStateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUpload.setState(null); + + // Create the InputDataUpload, which fails. + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + restInputDataUploadMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUpload.setCreationDate(null); + + // Create the InputDataUpload, which fails. + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + restInputDataUploadMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationUsernameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + inputDataUpload.setCreationUsername(null); + + // Create the InputDataUpload, which fails. + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + restInputDataUploadMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllInputDataUploads() throws Exception { + // Initialize the database + insertedInputDataUpload = inputDataUploadRepository.saveAndFlush(inputDataUpload); + + // Get all the inputDataUploadList + restInputDataUploadMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(inputDataUpload.getId().intValue()))) + .andExpect(jsonPath("$.[*].title").value(hasItem(DEFAULT_TITLE))) + .andExpect(jsonPath("$.[*].dataFileContentType").value(hasItem(DEFAULT_DATA_FILE_CONTENT_TYPE))) + .andExpect(jsonPath("$.[*].dataFile").value(hasItem(Base64.getEncoder().encodeToString(DEFAULT_DATA_FILE)))) + .andExpect(jsonPath("$.[*].uploadFileName").value(hasItem(DEFAULT_UPLOAD_FILE_NAME))) + .andExpect(jsonPath("$.[*].diskFileName").value(hasItem(DEFAULT_DISK_FILE_NAME))) + .andExpect(jsonPath("$.[*].type").value(hasItem(DEFAULT_TYPE.toString()))) + .andExpect(jsonPath("$.[*].state").value(hasItem(DEFAULT_STATE.toString()))) + .andExpect(jsonPath("$.[*].comments").value(hasItem(DEFAULT_COMMENTS))) + .andExpect(jsonPath("$.[*].creationDate").value(hasItem(DEFAULT_CREATION_DATE.toString()))) + .andExpect(jsonPath("$.[*].creationUsername").value(hasItem(DEFAULT_CREATION_USERNAME))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllInputDataUploadsWithEagerRelationshipsIsEnabled() throws Exception { + when(inputDataUploadServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restInputDataUploadMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(inputDataUploadServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllInputDataUploadsWithEagerRelationshipsIsNotEnabled() throws Exception { + when(inputDataUploadServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restInputDataUploadMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(inputDataUploadRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getInputDataUpload() throws Exception { + // Initialize the database + insertedInputDataUpload = inputDataUploadRepository.saveAndFlush(inputDataUpload); + + // Get the inputDataUpload + restInputDataUploadMockMvc + .perform(get(ENTITY_API_URL_ID, inputDataUpload.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(inputDataUpload.getId().intValue())) + .andExpect(jsonPath("$.title").value(DEFAULT_TITLE)) + .andExpect(jsonPath("$.dataFileContentType").value(DEFAULT_DATA_FILE_CONTENT_TYPE)) + .andExpect(jsonPath("$.dataFile").value(Base64.getEncoder().encodeToString(DEFAULT_DATA_FILE))) + .andExpect(jsonPath("$.uploadFileName").value(DEFAULT_UPLOAD_FILE_NAME)) + .andExpect(jsonPath("$.diskFileName").value(DEFAULT_DISK_FILE_NAME)) + .andExpect(jsonPath("$.type").value(DEFAULT_TYPE.toString())) + .andExpect(jsonPath("$.state").value(DEFAULT_STATE.toString())) + .andExpect(jsonPath("$.comments").value(DEFAULT_COMMENTS)) + .andExpect(jsonPath("$.creationDate").value(DEFAULT_CREATION_DATE.toString())) + .andExpect(jsonPath("$.creationUsername").value(DEFAULT_CREATION_USERNAME)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingInputDataUpload() throws Exception { + // Get the inputDataUpload + restInputDataUploadMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingInputDataUpload() throws Exception { + // Initialize the database + insertedInputDataUpload = inputDataUploadRepository.saveAndFlush(inputDataUpload); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputDataUpload + InputDataUpload updatedInputDataUpload = inputDataUploadRepository.findById(inputDataUpload.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedInputDataUpload are not directly saved in db + em.detach(updatedInputDataUpload); + updatedInputDataUpload + .title(UPDATED_TITLE) + .dataFile(UPDATED_DATA_FILE) + .dataFileContentType(UPDATED_DATA_FILE_CONTENT_TYPE) + .uploadFileName(UPDATED_UPLOAD_FILE_NAME) + .diskFileName(UPDATED_DISK_FILE_NAME) + .type(UPDATED_TYPE) + .state(UPDATED_STATE) + .comments(UPDATED_COMMENTS) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(updatedInputDataUpload); + + restInputDataUploadMockMvc + .perform( + put(ENTITY_API_URL_ID, inputDataUploadDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isOk()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedInputDataUploadToMatchAllProperties(updatedInputDataUpload); + } + + @Test + @Transactional + void putNonExistingInputDataUpload() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUpload.setId(longCount.incrementAndGet()); + + // Create the InputDataUpload + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restInputDataUploadMockMvc + .perform( + put(ENTITY_API_URL_ID, inputDataUploadDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchInputDataUpload() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUpload.setId(longCount.incrementAndGet()); + + // Create the InputDataUpload + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamInputDataUpload() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUpload.setId(longCount.incrementAndGet()); + + // Create the InputDataUpload + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateInputDataUploadWithPatch() throws Exception { + // Initialize the database + insertedInputDataUpload = inputDataUploadRepository.saveAndFlush(inputDataUpload); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputDataUpload using partial update + InputDataUpload partialUpdatedInputDataUpload = new InputDataUpload(); + partialUpdatedInputDataUpload.setId(inputDataUpload.getId()); + + partialUpdatedInputDataUpload + .title(UPDATED_TITLE) + .uploadFileName(UPDATED_UPLOAD_FILE_NAME) + .comments(UPDATED_COMMENTS) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME); + + restInputDataUploadMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedInputDataUpload.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedInputDataUpload)) + ) + .andExpect(status().isOk()); + + // Validate the InputDataUpload in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertInputDataUploadUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedInputDataUpload, inputDataUpload), + getPersistedInputDataUpload(inputDataUpload) + ); + } + + @Test + @Transactional + void fullUpdateInputDataUploadWithPatch() throws Exception { + // Initialize the database + insertedInputDataUpload = inputDataUploadRepository.saveAndFlush(inputDataUpload); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the inputDataUpload using partial update + InputDataUpload partialUpdatedInputDataUpload = new InputDataUpload(); + partialUpdatedInputDataUpload.setId(inputDataUpload.getId()); + + partialUpdatedInputDataUpload + .title(UPDATED_TITLE) + .dataFile(UPDATED_DATA_FILE) + .dataFileContentType(UPDATED_DATA_FILE_CONTENT_TYPE) + .uploadFileName(UPDATED_UPLOAD_FILE_NAME) + .diskFileName(UPDATED_DISK_FILE_NAME) + .type(UPDATED_TYPE) + .state(UPDATED_STATE) + .comments(UPDATED_COMMENTS) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + + restInputDataUploadMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedInputDataUpload.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedInputDataUpload)) + ) + .andExpect(status().isOk()); + + // Validate the InputDataUpload in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertInputDataUploadUpdatableFieldsEquals( + partialUpdatedInputDataUpload, + getPersistedInputDataUpload(partialUpdatedInputDataUpload) + ); + } + + @Test + @Transactional + void patchNonExistingInputDataUpload() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUpload.setId(longCount.incrementAndGet()); + + // Create the InputDataUpload + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restInputDataUploadMockMvc + .perform( + patch(ENTITY_API_URL_ID, inputDataUploadDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchInputDataUpload() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUpload.setId(longCount.incrementAndGet()); + + // Create the InputDataUpload + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamInputDataUpload() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + inputDataUpload.setId(longCount.incrementAndGet()); + + // Create the InputDataUpload + InputDataUploadDTO inputDataUploadDTO = inputDataUploadMapper.toDto(inputDataUpload); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restInputDataUploadMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(inputDataUploadDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the InputDataUpload in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteInputDataUpload() throws Exception { + // Initialize the database + insertedInputDataUpload = inputDataUploadRepository.saveAndFlush(inputDataUpload); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the inputDataUpload + restInputDataUploadMockMvc + .perform(delete(ENTITY_API_URL_ID, inputDataUpload.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return inputDataUploadRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected InputDataUpload getPersistedInputDataUpload(InputDataUpload inputDataUpload) { + return inputDataUploadRepository.findById(inputDataUpload.getId()).orElseThrow(); + } + + protected void assertPersistedInputDataUploadToMatchAllProperties(InputDataUpload expectedInputDataUpload) { + assertInputDataUploadAllPropertiesEquals(expectedInputDataUpload, getPersistedInputDataUpload(expectedInputDataUpload)); + } + + protected void assertPersistedInputDataUploadToMatchUpdatableProperties(InputDataUpload expectedInputDataUpload) { + assertInputDataUploadAllUpdatablePropertiesEquals(expectedInputDataUpload, getPersistedInputDataUpload(expectedInputDataUpload)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/MetadataPropertyResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/MetadataPropertyResourceIT.java new file mode 100644 index 0000000..1f07109 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/MetadataPropertyResourceIT.java @@ -0,0 +1,582 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.MetadataPropertyAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.MetadataProperty; +import com.oguerreiro.resilient.domain.enumeration.MetadataType; +import com.oguerreiro.resilient.repository.MetadataPropertyRepository; +import com.oguerreiro.resilient.service.dto.MetadataPropertyDTO; +import com.oguerreiro.resilient.service.mapper.MetadataPropertyMapper; +import jakarta.persistence.EntityManager; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link MetadataPropertyResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +@WithMockUser +class MetadataPropertyResourceIT { + + private static final String DEFAULT_CODE = "AAAAAAAAAA"; + private static final String UPDATED_CODE = "BBBBBBBBBB"; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final Boolean DEFAULT_MANDATORY = false; + private static final Boolean UPDATED_MANDATORY = true; + + private static final MetadataType DEFAULT_METADATA_TYPE = MetadataType.STRING; + private static final MetadataType UPDATED_METADATA_TYPE = MetadataType.BOOLEAN; + + private static final String DEFAULT_PATTERN = "AAAAAAAAAA"; + private static final String UPDATED_PATTERN = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/metadata-properties"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private MetadataPropertyRepository metadataPropertyRepository; + + @Autowired + private MetadataPropertyMapper metadataPropertyMapper; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restMetadataPropertyMockMvc; + + private MetadataProperty metadataProperty; + + private MetadataProperty insertedMetadataProperty; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static MetadataProperty createEntity(EntityManager em) { + MetadataProperty metadataProperty = new MetadataProperty() + .code(DEFAULT_CODE) + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .mandatory(DEFAULT_MANDATORY) + .metadataType(DEFAULT_METADATA_TYPE) + .pattern(DEFAULT_PATTERN) + .version(DEFAULT_VERSION); + return metadataProperty; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static MetadataProperty createUpdatedEntity(EntityManager em) { + MetadataProperty metadataProperty = new MetadataProperty() + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .mandatory(UPDATED_MANDATORY) + .metadataType(UPDATED_METADATA_TYPE) + .pattern(UPDATED_PATTERN) + .version(UPDATED_VERSION); + return metadataProperty; + } + + @BeforeEach + public void initTest() { + metadataProperty = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedMetadataProperty != null) { + metadataPropertyRepository.delete(insertedMetadataProperty); + insertedMetadataProperty = null; + } + } + + @Test + @Transactional + void createMetadataProperty() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the MetadataProperty + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + var returnedMetadataPropertyDTO = om.readValue( + restMetadataPropertyMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + MetadataPropertyDTO.class + ); + + // Validate the MetadataProperty in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedMetadataProperty = metadataPropertyMapper.toEntity(returnedMetadataPropertyDTO); + assertMetadataPropertyUpdatableFieldsEquals(returnedMetadataProperty, getPersistedMetadataProperty(returnedMetadataProperty)); + + insertedMetadataProperty = returnedMetadataProperty; + } + + @Test + @Transactional + void createMetadataPropertyWithExistingId() throws Exception { + // Create the MetadataProperty with an existing ID + metadataProperty.setId(1L); + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restMetadataPropertyMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + metadataProperty.setCode(null); + + // Create the MetadataProperty, which fails. + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + restMetadataPropertyMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + metadataProperty.setName(null); + + // Create the MetadataProperty, which fails. + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + restMetadataPropertyMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkMetadataTypeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + metadataProperty.setMetadataType(null); + + // Create the MetadataProperty, which fails. + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + restMetadataPropertyMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllMetadataProperties() throws Exception { + // Initialize the database + insertedMetadataProperty = metadataPropertyRepository.saveAndFlush(metadataProperty); + + // Get all the metadataPropertyList + restMetadataPropertyMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(metadataProperty.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].mandatory").value(hasItem(DEFAULT_MANDATORY.booleanValue()))) + .andExpect(jsonPath("$.[*].metadataType").value(hasItem(DEFAULT_METADATA_TYPE.toString()))) + .andExpect(jsonPath("$.[*].pattern").value(hasItem(DEFAULT_PATTERN))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @Test + @Transactional + void getMetadataProperty() throws Exception { + // Initialize the database + insertedMetadataProperty = metadataPropertyRepository.saveAndFlush(metadataProperty); + + // Get the metadataProperty + restMetadataPropertyMockMvc + .perform(get(ENTITY_API_URL_ID, metadataProperty.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(metadataProperty.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.mandatory").value(DEFAULT_MANDATORY.booleanValue())) + .andExpect(jsonPath("$.metadataType").value(DEFAULT_METADATA_TYPE.toString())) + .andExpect(jsonPath("$.pattern").value(DEFAULT_PATTERN)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingMetadataProperty() throws Exception { + // Get the metadataProperty + restMetadataPropertyMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingMetadataProperty() throws Exception { + // Initialize the database + insertedMetadataProperty = metadataPropertyRepository.saveAndFlush(metadataProperty); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the metadataProperty + MetadataProperty updatedMetadataProperty = metadataPropertyRepository.findById(metadataProperty.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedMetadataProperty are not directly saved in db + em.detach(updatedMetadataProperty); + updatedMetadataProperty + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .mandatory(UPDATED_MANDATORY) + .metadataType(UPDATED_METADATA_TYPE) + .pattern(UPDATED_PATTERN) + .version(UPDATED_VERSION); + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(updatedMetadataProperty); + + restMetadataPropertyMockMvc + .perform( + put(ENTITY_API_URL_ID, metadataPropertyDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isOk()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedMetadataPropertyToMatchAllProperties(updatedMetadataProperty); + } + + @Test + @Transactional + void putNonExistingMetadataProperty() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataProperty.setId(longCount.incrementAndGet()); + + // Create the MetadataProperty + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restMetadataPropertyMockMvc + .perform( + put(ENTITY_API_URL_ID, metadataPropertyDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchMetadataProperty() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataProperty.setId(longCount.incrementAndGet()); + + // Create the MetadataProperty + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataPropertyMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamMetadataProperty() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataProperty.setId(longCount.incrementAndGet()); + + // Create the MetadataProperty + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataPropertyMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateMetadataPropertyWithPatch() throws Exception { + // Initialize the database + insertedMetadataProperty = metadataPropertyRepository.saveAndFlush(metadataProperty); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the metadataProperty using partial update + MetadataProperty partialUpdatedMetadataProperty = new MetadataProperty(); + partialUpdatedMetadataProperty.setId(metadataProperty.getId()); + + partialUpdatedMetadataProperty + .name(UPDATED_NAME) + .mandatory(UPDATED_MANDATORY) + .metadataType(UPDATED_METADATA_TYPE) + .version(UPDATED_VERSION); + + restMetadataPropertyMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedMetadataProperty.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedMetadataProperty)) + ) + .andExpect(status().isOk()); + + // Validate the MetadataProperty in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertMetadataPropertyUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedMetadataProperty, metadataProperty), + getPersistedMetadataProperty(metadataProperty) + ); + } + + @Test + @Transactional + void fullUpdateMetadataPropertyWithPatch() throws Exception { + // Initialize the database + insertedMetadataProperty = metadataPropertyRepository.saveAndFlush(metadataProperty); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the metadataProperty using partial update + MetadataProperty partialUpdatedMetadataProperty = new MetadataProperty(); + partialUpdatedMetadataProperty.setId(metadataProperty.getId()); + + partialUpdatedMetadataProperty + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .mandatory(UPDATED_MANDATORY) + .metadataType(UPDATED_METADATA_TYPE) + .pattern(UPDATED_PATTERN) + .version(UPDATED_VERSION); + + restMetadataPropertyMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedMetadataProperty.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedMetadataProperty)) + ) + .andExpect(status().isOk()); + + // Validate the MetadataProperty in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertMetadataPropertyUpdatableFieldsEquals( + partialUpdatedMetadataProperty, + getPersistedMetadataProperty(partialUpdatedMetadataProperty) + ); + } + + @Test + @Transactional + void patchNonExistingMetadataProperty() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataProperty.setId(longCount.incrementAndGet()); + + // Create the MetadataProperty + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restMetadataPropertyMockMvc + .perform( + patch(ENTITY_API_URL_ID, metadataPropertyDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchMetadataProperty() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataProperty.setId(longCount.incrementAndGet()); + + // Create the MetadataProperty + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataPropertyMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamMetadataProperty() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataProperty.setId(longCount.incrementAndGet()); + + // Create the MetadataProperty + MetadataPropertyDTO metadataPropertyDTO = metadataPropertyMapper.toDto(metadataProperty); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataPropertyMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(metadataPropertyDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the MetadataProperty in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteMetadataProperty() throws Exception { + // Initialize the database + insertedMetadataProperty = metadataPropertyRepository.saveAndFlush(metadataProperty); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the metadataProperty + restMetadataPropertyMockMvc + .perform(delete(ENTITY_API_URL_ID, metadataProperty.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return metadataPropertyRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected MetadataProperty getPersistedMetadataProperty(MetadataProperty metadataProperty) { + return metadataPropertyRepository.findById(metadataProperty.getId()).orElseThrow(); + } + + protected void assertPersistedMetadataPropertyToMatchAllProperties(MetadataProperty expectedMetadataProperty) { + assertMetadataPropertyAllPropertiesEquals(expectedMetadataProperty, getPersistedMetadataProperty(expectedMetadataProperty)); + } + + protected void assertPersistedMetadataPropertyToMatchUpdatableProperties(MetadataProperty expectedMetadataProperty) { + assertMetadataPropertyAllUpdatablePropertiesEquals( + expectedMetadataProperty, + getPersistedMetadataProperty(expectedMetadataProperty) + ); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/MetadataValueResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/MetadataValueResourceIT.java new file mode 100644 index 0000000..97a4683 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/MetadataValueResourceIT.java @@ -0,0 +1,553 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.MetadataValueAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.MetadataValue; +import com.oguerreiro.resilient.repository.MetadataValueRepository; +import com.oguerreiro.resilient.service.dto.MetadataValueDTO; +import com.oguerreiro.resilient.service.mapper.MetadataValueMapper; +import jakarta.persistence.EntityManager; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link MetadataValueResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +@WithMockUser +class MetadataValueResourceIT { + + private static final String DEFAULT_METADATA_PROPERTY_CODE = "AAAAAAAAAA"; + private static final String UPDATED_METADATA_PROPERTY_CODE = "BBBBBBBBBB"; + + private static final String DEFAULT_TARGET_DOMAIN_KEY = "AAAAAAAAAA"; + private static final String UPDATED_TARGET_DOMAIN_KEY = "BBBBBBBBBB"; + + private static final Long DEFAULT_TARGET_DOMAIN_ID = 1L; + private static final Long UPDATED_TARGET_DOMAIN_ID = 2L; + + private static final String DEFAULT_VALUE = "AAAAAAAAAA"; + private static final String UPDATED_VALUE = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/metadata-values"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private MetadataValueRepository metadataValueRepository; + + @Autowired + private MetadataValueMapper metadataValueMapper; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restMetadataValueMockMvc; + + private MetadataValue metadataValue; + + private MetadataValue insertedMetadataValue; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static MetadataValue createEntity(EntityManager em) { + MetadataValue metadataValue = new MetadataValue() + .metadataPropertyCode(DEFAULT_METADATA_PROPERTY_CODE) + .targetDomainKey(DEFAULT_TARGET_DOMAIN_KEY) + .targetDomainId(DEFAULT_TARGET_DOMAIN_ID) + .value(DEFAULT_VALUE) + .version(DEFAULT_VERSION); + return metadataValue; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static MetadataValue createUpdatedEntity(EntityManager em) { + MetadataValue metadataValue = new MetadataValue() + .metadataPropertyCode(UPDATED_METADATA_PROPERTY_CODE) + .targetDomainKey(UPDATED_TARGET_DOMAIN_KEY) + .targetDomainId(UPDATED_TARGET_DOMAIN_ID) + .value(UPDATED_VALUE) + .version(UPDATED_VERSION); + return metadataValue; + } + + @BeforeEach + public void initTest() { + metadataValue = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedMetadataValue != null) { + metadataValueRepository.delete(insertedMetadataValue); + insertedMetadataValue = null; + } + } + + @Test + @Transactional + void createMetadataValue() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the MetadataValue + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + var returnedMetadataValueDTO = om.readValue( + restMetadataValueMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + MetadataValueDTO.class + ); + + // Validate the MetadataValue in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedMetadataValue = metadataValueMapper.toEntity(returnedMetadataValueDTO); + assertMetadataValueUpdatableFieldsEquals(returnedMetadataValue, getPersistedMetadataValue(returnedMetadataValue)); + + insertedMetadataValue = returnedMetadataValue; + } + + @Test + @Transactional + void createMetadataValueWithExistingId() throws Exception { + // Create the MetadataValue with an existing ID + metadataValue.setId(1L); + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restMetadataValueMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkMetadataPropertyCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + metadataValue.setMetadataPropertyCode(null); + + // Create the MetadataValue, which fails. + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + restMetadataValueMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkTargetDomainKeyIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + metadataValue.setTargetDomainKey(null); + + // Create the MetadataValue, which fails. + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + restMetadataValueMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkTargetDomainIdIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + metadataValue.setTargetDomainId(null); + + // Create the MetadataValue, which fails. + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + restMetadataValueMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllMetadataValues() throws Exception { + // Initialize the database + insertedMetadataValue = metadataValueRepository.saveAndFlush(metadataValue); + + // Get all the metadataValueList + restMetadataValueMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(metadataValue.getId().intValue()))) + .andExpect(jsonPath("$.[*].metadataPropertyCode").value(hasItem(DEFAULT_METADATA_PROPERTY_CODE))) + .andExpect(jsonPath("$.[*].targetDomainKey").value(hasItem(DEFAULT_TARGET_DOMAIN_KEY))) + .andExpect(jsonPath("$.[*].targetDomainId").value(hasItem(DEFAULT_TARGET_DOMAIN_ID.intValue()))) + .andExpect(jsonPath("$.[*].value").value(hasItem(DEFAULT_VALUE))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @Test + @Transactional + void getMetadataValue() throws Exception { + // Initialize the database + insertedMetadataValue = metadataValueRepository.saveAndFlush(metadataValue); + + // Get the metadataValue + restMetadataValueMockMvc + .perform(get(ENTITY_API_URL_ID, metadataValue.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(metadataValue.getId().intValue())) + .andExpect(jsonPath("$.metadataPropertyCode").value(DEFAULT_METADATA_PROPERTY_CODE)) + .andExpect(jsonPath("$.targetDomainKey").value(DEFAULT_TARGET_DOMAIN_KEY)) + .andExpect(jsonPath("$.targetDomainId").value(DEFAULT_TARGET_DOMAIN_ID.intValue())) + .andExpect(jsonPath("$.value").value(DEFAULT_VALUE)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingMetadataValue() throws Exception { + // Get the metadataValue + restMetadataValueMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingMetadataValue() throws Exception { + // Initialize the database + insertedMetadataValue = metadataValueRepository.saveAndFlush(metadataValue); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the metadataValue + MetadataValue updatedMetadataValue = metadataValueRepository.findById(metadataValue.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedMetadataValue are not directly saved in db + em.detach(updatedMetadataValue); + updatedMetadataValue + .metadataPropertyCode(UPDATED_METADATA_PROPERTY_CODE) + .targetDomainKey(UPDATED_TARGET_DOMAIN_KEY) + .targetDomainId(UPDATED_TARGET_DOMAIN_ID) + .value(UPDATED_VALUE) + .version(UPDATED_VERSION); + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(updatedMetadataValue); + + restMetadataValueMockMvc + .perform( + put(ENTITY_API_URL_ID, metadataValueDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isOk()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedMetadataValueToMatchAllProperties(updatedMetadataValue); + } + + @Test + @Transactional + void putNonExistingMetadataValue() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataValue.setId(longCount.incrementAndGet()); + + // Create the MetadataValue + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restMetadataValueMockMvc + .perform( + put(ENTITY_API_URL_ID, metadataValueDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchMetadataValue() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataValue.setId(longCount.incrementAndGet()); + + // Create the MetadataValue + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataValueMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamMetadataValue() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataValue.setId(longCount.incrementAndGet()); + + // Create the MetadataValue + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataValueMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateMetadataValueWithPatch() throws Exception { + // Initialize the database + insertedMetadataValue = metadataValueRepository.saveAndFlush(metadataValue); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the metadataValue using partial update + MetadataValue partialUpdatedMetadataValue = new MetadataValue(); + partialUpdatedMetadataValue.setId(metadataValue.getId()); + + partialUpdatedMetadataValue.targetDomainId(UPDATED_TARGET_DOMAIN_ID).value(UPDATED_VALUE); + + restMetadataValueMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedMetadataValue.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedMetadataValue)) + ) + .andExpect(status().isOk()); + + // Validate the MetadataValue in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertMetadataValueUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedMetadataValue, metadataValue), + getPersistedMetadataValue(metadataValue) + ); + } + + @Test + @Transactional + void fullUpdateMetadataValueWithPatch() throws Exception { + // Initialize the database + insertedMetadataValue = metadataValueRepository.saveAndFlush(metadataValue); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the metadataValue using partial update + MetadataValue partialUpdatedMetadataValue = new MetadataValue(); + partialUpdatedMetadataValue.setId(metadataValue.getId()); + + partialUpdatedMetadataValue + .metadataPropertyCode(UPDATED_METADATA_PROPERTY_CODE) + .targetDomainKey(UPDATED_TARGET_DOMAIN_KEY) + .targetDomainId(UPDATED_TARGET_DOMAIN_ID) + .value(UPDATED_VALUE) + .version(UPDATED_VERSION); + + restMetadataValueMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedMetadataValue.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedMetadataValue)) + ) + .andExpect(status().isOk()); + + // Validate the MetadataValue in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertMetadataValueUpdatableFieldsEquals(partialUpdatedMetadataValue, getPersistedMetadataValue(partialUpdatedMetadataValue)); + } + + @Test + @Transactional + void patchNonExistingMetadataValue() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataValue.setId(longCount.incrementAndGet()); + + // Create the MetadataValue + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restMetadataValueMockMvc + .perform( + patch(ENTITY_API_URL_ID, metadataValueDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchMetadataValue() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataValue.setId(longCount.incrementAndGet()); + + // Create the MetadataValue + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataValueMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamMetadataValue() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + metadataValue.setId(longCount.incrementAndGet()); + + // Create the MetadataValue + MetadataValueDTO metadataValueDTO = metadataValueMapper.toDto(metadataValue); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restMetadataValueMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(metadataValueDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the MetadataValue in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteMetadataValue() throws Exception { + // Initialize the database + insertedMetadataValue = metadataValueRepository.saveAndFlush(metadataValue); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the metadataValue + restMetadataValueMockMvc + .perform(delete(ENTITY_API_URL_ID, metadataValue.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return metadataValueRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected MetadataValue getPersistedMetadataValue(MetadataValue metadataValue) { + return metadataValueRepository.findById(metadataValue.getId()).orElseThrow(); + } + + protected void assertPersistedMetadataValueToMatchAllProperties(MetadataValue expectedMetadataValue) { + assertMetadataValueAllPropertiesEquals(expectedMetadataValue, getPersistedMetadataValue(expectedMetadataValue)); + } + + protected void assertPersistedMetadataValueToMatchUpdatableProperties(MetadataValue expectedMetadataValue) { + assertMetadataValueAllUpdatablePropertiesEquals(expectedMetadataValue, getPersistedMetadataValue(expectedMetadataValue)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/OrganizationResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/OrganizationResourceIT.java new file mode 100644 index 0000000..94c1bdb --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/OrganizationResourceIT.java @@ -0,0 +1,589 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.OrganizationAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.Organization; +import com.oguerreiro.resilient.repository.OrganizationRepository; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.service.OrganizationService; +import com.oguerreiro.resilient.service.dto.OrganizationDTO; +import com.oguerreiro.resilient.service.mapper.OrganizationMapper; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link OrganizationResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class OrganizationResourceIT { + + private static final String DEFAULT_CODE = "AAAAAAAAAA"; + private static final String UPDATED_CODE = "BBBBBBBBBB"; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final byte[] DEFAULT_IMAGE = TestUtil.createByteArray(1, "0"); + private static final byte[] UPDATED_IMAGE = TestUtil.createByteArray(1, "1"); + private static final String DEFAULT_IMAGE_CONTENT_TYPE = "image/jpg"; + private static final String UPDATED_IMAGE_CONTENT_TYPE = "image/png"; + + private static final Boolean DEFAULT_INPUT_INVENTORY = false; + private static final Boolean UPDATED_INPUT_INVENTORY = true; + + private static final Boolean DEFAULT_OUTPUT_INVENTORY = false; + private static final Boolean UPDATED_OUTPUT_INVENTORY = true; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/organizations"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private OrganizationRepository organizationRepository; + + @Autowired + private UserRepository userRepository; + + @Mock + private OrganizationRepository organizationRepositoryMock; + + @Autowired + private OrganizationMapper organizationMapper; + + @Mock + private OrganizationService organizationServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restOrganizationMockMvc; + + private Organization organization; + + private Organization insertedOrganization; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Organization createEntity(EntityManager em) { + Organization organization = new Organization() + .code(DEFAULT_CODE) + .name(DEFAULT_NAME) + .image(DEFAULT_IMAGE) + .imageContentType(DEFAULT_IMAGE_CONTENT_TYPE) + .inputInventory(DEFAULT_INPUT_INVENTORY) + .outputInventory(DEFAULT_OUTPUT_INVENTORY) + .version(DEFAULT_VERSION); + return organization; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Organization createUpdatedEntity(EntityManager em) { + Organization organization = new Organization() + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .image(UPDATED_IMAGE) + .imageContentType(UPDATED_IMAGE_CONTENT_TYPE) + .inputInventory(UPDATED_INPUT_INVENTORY) + .outputInventory(UPDATED_OUTPUT_INVENTORY) + .version(UPDATED_VERSION); + return organization; + } + + @BeforeEach + public void initTest() { + organization = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedOrganization != null) { + organizationRepository.delete(insertedOrganization); + insertedOrganization = null; + } + } + + @Test + @Transactional + void createOrganization() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the Organization + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + var returnedOrganizationDTO = om.readValue( + restOrganizationMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + OrganizationDTO.class + ); + + // Validate the Organization in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedOrganization = organizationMapper.toEntity(returnedOrganizationDTO); + assertOrganizationUpdatableFieldsEquals(returnedOrganization, getPersistedOrganization(returnedOrganization)); + + insertedOrganization = returnedOrganization; + } + + @Test + @Transactional + void createOrganizationWithExistingId() throws Exception { + // Create the Organization with an existing ID + organization.setId(1L); + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restOrganizationMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + organization.setCode(null); + + // Create the Organization, which fails. + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + restOrganizationMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + organization.setName(null); + + // Create the Organization, which fails. + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + restOrganizationMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllOrganizations() throws Exception { + // Initialize the database + insertedOrganization = organizationRepository.saveAndFlush(organization); + + // Get all the organizationList + restOrganizationMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(organization.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].imageContentType").value(hasItem(DEFAULT_IMAGE_CONTENT_TYPE))) + .andExpect(jsonPath("$.[*].image").value(hasItem(Base64.getEncoder().encodeToString(DEFAULT_IMAGE)))) + .andExpect(jsonPath("$.[*].inputInventory").value(hasItem(DEFAULT_INPUT_INVENTORY.booleanValue()))) + .andExpect(jsonPath("$.[*].outputInventory").value(hasItem(DEFAULT_OUTPUT_INVENTORY.booleanValue()))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllOrganizationsWithEagerRelationshipsIsEnabled() throws Exception { + when(organizationServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restOrganizationMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(organizationServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllOrganizationsWithEagerRelationshipsIsNotEnabled() throws Exception { + when(organizationServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restOrganizationMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(organizationRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getOrganization() throws Exception { + // Initialize the database + insertedOrganization = organizationRepository.saveAndFlush(organization); + + // Get the organization + restOrganizationMockMvc + .perform(get(ENTITY_API_URL_ID, organization.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(organization.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.imageContentType").value(DEFAULT_IMAGE_CONTENT_TYPE)) + .andExpect(jsonPath("$.image").value(Base64.getEncoder().encodeToString(DEFAULT_IMAGE))) + .andExpect(jsonPath("$.inputInventory").value(DEFAULT_INPUT_INVENTORY.booleanValue())) + .andExpect(jsonPath("$.outputInventory").value(DEFAULT_OUTPUT_INVENTORY.booleanValue())) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingOrganization() throws Exception { + // Get the organization + restOrganizationMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingOrganization() throws Exception { + // Initialize the database + insertedOrganization = organizationRepository.saveAndFlush(organization); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the organization + Organization updatedOrganization = organizationRepository.findById(organization.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedOrganization are not directly saved in db + em.detach(updatedOrganization); + updatedOrganization + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .image(UPDATED_IMAGE) + .imageContentType(UPDATED_IMAGE_CONTENT_TYPE) + .inputInventory(UPDATED_INPUT_INVENTORY) + .outputInventory(UPDATED_OUTPUT_INVENTORY) + .version(UPDATED_VERSION); + OrganizationDTO organizationDTO = organizationMapper.toDto(updatedOrganization); + + restOrganizationMockMvc + .perform( + put(ENTITY_API_URL_ID, organizationDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isOk()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedOrganizationToMatchAllProperties(updatedOrganization); + } + + @Test + @Transactional + void putNonExistingOrganization() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organization.setId(longCount.incrementAndGet()); + + // Create the Organization + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restOrganizationMockMvc + .perform( + put(ENTITY_API_URL_ID, organizationDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchOrganization() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organization.setId(longCount.incrementAndGet()); + + // Create the Organization + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamOrganization() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organization.setId(longCount.incrementAndGet()); + + // Create the Organization + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateOrganizationWithPatch() throws Exception { + // Initialize the database + insertedOrganization = organizationRepository.saveAndFlush(organization); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the organization using partial update + Organization partialUpdatedOrganization = new Organization(); + partialUpdatedOrganization.setId(organization.getId()); + + partialUpdatedOrganization + .code(UPDATED_CODE) + .inputInventory(UPDATED_INPUT_INVENTORY) + .outputInventory(UPDATED_OUTPUT_INVENTORY) + .version(UPDATED_VERSION); + + restOrganizationMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedOrganization.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedOrganization)) + ) + .andExpect(status().isOk()); + + // Validate the Organization in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertOrganizationUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedOrganization, organization), + getPersistedOrganization(organization) + ); + } + + @Test + @Transactional + void fullUpdateOrganizationWithPatch() throws Exception { + // Initialize the database + insertedOrganization = organizationRepository.saveAndFlush(organization); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the organization using partial update + Organization partialUpdatedOrganization = new Organization(); + partialUpdatedOrganization.setId(organization.getId()); + + partialUpdatedOrganization + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .image(UPDATED_IMAGE) + .imageContentType(UPDATED_IMAGE_CONTENT_TYPE) + .inputInventory(UPDATED_INPUT_INVENTORY) + .outputInventory(UPDATED_OUTPUT_INVENTORY) + .version(UPDATED_VERSION); + + restOrganizationMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedOrganization.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedOrganization)) + ) + .andExpect(status().isOk()); + + // Validate the Organization in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertOrganizationUpdatableFieldsEquals(partialUpdatedOrganization, getPersistedOrganization(partialUpdatedOrganization)); + } + + @Test + @Transactional + void patchNonExistingOrganization() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organization.setId(longCount.incrementAndGet()); + + // Create the Organization + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restOrganizationMockMvc + .perform( + patch(ENTITY_API_URL_ID, organizationDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchOrganization() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organization.setId(longCount.incrementAndGet()); + + // Create the Organization + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamOrganization() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organization.setId(longCount.incrementAndGet()); + + // Create the Organization + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(organizationDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Organization in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteOrganization() throws Exception { + // Initialize the database + insertedOrganization = organizationRepository.saveAndFlush(organization); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the organization + restOrganizationMockMvc + .perform(delete(ENTITY_API_URL_ID, organization.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return organizationRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected Organization getPersistedOrganization(Organization organization) { + return organizationRepository.findById(organization.getId()).orElseThrow(); + } + + protected void assertPersistedOrganizationToMatchAllProperties(Organization expectedOrganization) { + assertOrganizationAllPropertiesEquals(expectedOrganization, getPersistedOrganization(expectedOrganization)); + } + + protected void assertPersistedOrganizationToMatchUpdatableProperties(Organization expectedOrganization) { + assertOrganizationAllUpdatablePropertiesEquals(expectedOrganization, getPersistedOrganization(expectedOrganization)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/OrganizationTypeResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/OrganizationTypeResourceIT.java new file mode 100644 index 0000000..a9248bb --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/OrganizationTypeResourceIT.java @@ -0,0 +1,629 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.OrganizationTypeAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.OrganizationType; +import com.oguerreiro.resilient.domain.enumeration.OrganizationNature; +import com.oguerreiro.resilient.repository.OrganizationTypeRepository; +import com.oguerreiro.resilient.service.OrganizationTypeService; +import com.oguerreiro.resilient.service.dto.OrganizationTypeDTO; +import com.oguerreiro.resilient.service.mapper.OrganizationTypeMapper; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link OrganizationTypeResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class OrganizationTypeResourceIT { + + private static final String DEFAULT_CODE = "AAAAAAAAAA"; + private static final String UPDATED_CODE = "BBBBBBBBBB"; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final OrganizationNature DEFAULT_NATURE = OrganizationNature.ORGANIZATION; + private static final OrganizationNature UPDATED_NATURE = OrganizationNature.PERSON; + + private static final byte[] DEFAULT_ICON = TestUtil.createByteArray(1, "0"); + private static final byte[] UPDATED_ICON = TestUtil.createByteArray(1, "1"); + private static final String DEFAULT_ICON_CONTENT_TYPE = "image/jpg"; + private static final String UPDATED_ICON_CONTENT_TYPE = "image/png"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/organization-types"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private OrganizationTypeRepository organizationTypeRepository; + + @Mock + private OrganizationTypeRepository organizationTypeRepositoryMock; + + @Autowired + private OrganizationTypeMapper organizationTypeMapper; + + @Mock + private OrganizationTypeService organizationTypeServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restOrganizationTypeMockMvc; + + private OrganizationType organizationType; + + private OrganizationType insertedOrganizationType; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static OrganizationType createEntity(EntityManager em) { + OrganizationType organizationType = new OrganizationType() + .code(DEFAULT_CODE) + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .nature(DEFAULT_NATURE) + .icon(DEFAULT_ICON) + .iconContentType(DEFAULT_ICON_CONTENT_TYPE) + .version(DEFAULT_VERSION); + return organizationType; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static OrganizationType createUpdatedEntity(EntityManager em) { + OrganizationType organizationType = new OrganizationType() + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .nature(UPDATED_NATURE) + .icon(UPDATED_ICON) + .iconContentType(UPDATED_ICON_CONTENT_TYPE) + .version(UPDATED_VERSION); + return organizationType; + } + + @BeforeEach + public void initTest() { + organizationType = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedOrganizationType != null) { + organizationTypeRepository.delete(insertedOrganizationType); + insertedOrganizationType = null; + } + } + + @Test + @Transactional + void createOrganizationType() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the OrganizationType + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + var returnedOrganizationTypeDTO = om.readValue( + restOrganizationTypeMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + OrganizationTypeDTO.class + ); + + // Validate the OrganizationType in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedOrganizationType = organizationTypeMapper.toEntity(returnedOrganizationTypeDTO); + assertOrganizationTypeUpdatableFieldsEquals(returnedOrganizationType, getPersistedOrganizationType(returnedOrganizationType)); + + insertedOrganizationType = returnedOrganizationType; + } + + @Test + @Transactional + void createOrganizationTypeWithExistingId() throws Exception { + // Create the OrganizationType with an existing ID + organizationType.setId(1L); + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restOrganizationTypeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + organizationType.setCode(null); + + // Create the OrganizationType, which fails. + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + restOrganizationTypeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + organizationType.setName(null); + + // Create the OrganizationType, which fails. + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + restOrganizationTypeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkDescriptionIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + organizationType.setDescription(null); + + // Create the OrganizationType, which fails. + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + restOrganizationTypeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNatureIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + organizationType.setNature(null); + + // Create the OrganizationType, which fails. + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + restOrganizationTypeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllOrganizationTypes() throws Exception { + // Initialize the database + insertedOrganizationType = organizationTypeRepository.saveAndFlush(organizationType); + + // Get all the organizationTypeList + restOrganizationTypeMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(organizationType.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].nature").value(hasItem(DEFAULT_NATURE.toString()))) + .andExpect(jsonPath("$.[*].iconContentType").value(hasItem(DEFAULT_ICON_CONTENT_TYPE))) + .andExpect(jsonPath("$.[*].icon").value(hasItem(Base64.getEncoder().encodeToString(DEFAULT_ICON)))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllOrganizationTypesWithEagerRelationshipsIsEnabled() throws Exception { + when(organizationTypeServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restOrganizationTypeMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(organizationTypeServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllOrganizationTypesWithEagerRelationshipsIsNotEnabled() throws Exception { + when(organizationTypeServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restOrganizationTypeMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(organizationTypeRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getOrganizationType() throws Exception { + // Initialize the database + insertedOrganizationType = organizationTypeRepository.saveAndFlush(organizationType); + + // Get the organizationType + restOrganizationTypeMockMvc + .perform(get(ENTITY_API_URL_ID, organizationType.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(organizationType.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.nature").value(DEFAULT_NATURE.toString())) + .andExpect(jsonPath("$.iconContentType").value(DEFAULT_ICON_CONTENT_TYPE)) + .andExpect(jsonPath("$.icon").value(Base64.getEncoder().encodeToString(DEFAULT_ICON))) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingOrganizationType() throws Exception { + // Get the organizationType + restOrganizationTypeMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingOrganizationType() throws Exception { + // Initialize the database + insertedOrganizationType = organizationTypeRepository.saveAndFlush(organizationType); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the organizationType + OrganizationType updatedOrganizationType = organizationTypeRepository.findById(organizationType.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedOrganizationType are not directly saved in db + em.detach(updatedOrganizationType); + updatedOrganizationType + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .nature(UPDATED_NATURE) + .icon(UPDATED_ICON) + .iconContentType(UPDATED_ICON_CONTENT_TYPE) + .version(UPDATED_VERSION); + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(updatedOrganizationType); + + restOrganizationTypeMockMvc + .perform( + put(ENTITY_API_URL_ID, organizationTypeDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isOk()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedOrganizationTypeToMatchAllProperties(updatedOrganizationType); + } + + @Test + @Transactional + void putNonExistingOrganizationType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organizationType.setId(longCount.incrementAndGet()); + + // Create the OrganizationType + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restOrganizationTypeMockMvc + .perform( + put(ENTITY_API_URL_ID, organizationTypeDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchOrganizationType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organizationType.setId(longCount.incrementAndGet()); + + // Create the OrganizationType + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationTypeMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamOrganizationType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organizationType.setId(longCount.incrementAndGet()); + + // Create the OrganizationType + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationTypeMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateOrganizationTypeWithPatch() throws Exception { + // Initialize the database + insertedOrganizationType = organizationTypeRepository.saveAndFlush(organizationType); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the organizationType using partial update + OrganizationType partialUpdatedOrganizationType = new OrganizationType(); + partialUpdatedOrganizationType.setId(organizationType.getId()); + + partialUpdatedOrganizationType.code(UPDATED_CODE); + + restOrganizationTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedOrganizationType.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedOrganizationType)) + ) + .andExpect(status().isOk()); + + // Validate the OrganizationType in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertOrganizationTypeUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedOrganizationType, organizationType), + getPersistedOrganizationType(organizationType) + ); + } + + @Test + @Transactional + void fullUpdateOrganizationTypeWithPatch() throws Exception { + // Initialize the database + insertedOrganizationType = organizationTypeRepository.saveAndFlush(organizationType); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the organizationType using partial update + OrganizationType partialUpdatedOrganizationType = new OrganizationType(); + partialUpdatedOrganizationType.setId(organizationType.getId()); + + partialUpdatedOrganizationType + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .nature(UPDATED_NATURE) + .icon(UPDATED_ICON) + .iconContentType(UPDATED_ICON_CONTENT_TYPE) + .version(UPDATED_VERSION); + + restOrganizationTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedOrganizationType.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedOrganizationType)) + ) + .andExpect(status().isOk()); + + // Validate the OrganizationType in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertOrganizationTypeUpdatableFieldsEquals( + partialUpdatedOrganizationType, + getPersistedOrganizationType(partialUpdatedOrganizationType) + ); + } + + @Test + @Transactional + void patchNonExistingOrganizationType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organizationType.setId(longCount.incrementAndGet()); + + // Create the OrganizationType + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restOrganizationTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, organizationTypeDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchOrganizationType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organizationType.setId(longCount.incrementAndGet()); + + // Create the OrganizationType + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamOrganizationType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + organizationType.setId(longCount.incrementAndGet()); + + // Create the OrganizationType + OrganizationTypeDTO organizationTypeDTO = organizationTypeMapper.toDto(organizationType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOrganizationTypeMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(organizationTypeDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the OrganizationType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteOrganizationType() throws Exception { + // Initialize the database + insertedOrganizationType = organizationTypeRepository.saveAndFlush(organizationType); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the organizationType + restOrganizationTypeMockMvc + .perform(delete(ENTITY_API_URL_ID, organizationType.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return organizationTypeRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected OrganizationType getPersistedOrganizationType(OrganizationType organizationType) { + return organizationTypeRepository.findById(organizationType.getId()).orElseThrow(); + } + + protected void assertPersistedOrganizationTypeToMatchAllProperties(OrganizationType expectedOrganizationType) { + assertOrganizationTypeAllPropertiesEquals(expectedOrganizationType, getPersistedOrganizationType(expectedOrganizationType)); + } + + protected void assertPersistedOrganizationTypeToMatchUpdatableProperties(OrganizationType expectedOrganizationType) { + assertOrganizationTypeAllUpdatablePropertiesEquals( + expectedOrganizationType, + getPersistedOrganizationType(expectedOrganizationType) + ); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/OutputDataResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/OutputDataResourceIT.java new file mode 100644 index 0000000..89fd745 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/OutputDataResourceIT.java @@ -0,0 +1,500 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.OutputDataAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static com.oguerreiro.resilient.web.rest.TestUtil.sameNumber; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.OutputData; +import com.oguerreiro.resilient.repository.OutputDataRepository; +import com.oguerreiro.resilient.service.OutputDataService; +import com.oguerreiro.resilient.service.dto.OutputDataDTO; +import com.oguerreiro.resilient.service.mapper.OutputDataMapper; +import jakarta.persistence.EntityManager; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link OutputDataResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class OutputDataResourceIT { + + private static final BigDecimal DEFAULT_VALUE = new BigDecimal(1); + private static final BigDecimal UPDATED_VALUE = new BigDecimal(2); + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/output-data"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private OutputDataRepository outputDataRepository; + + @Mock + private OutputDataRepository outputDataRepositoryMock; + + @Autowired + private OutputDataMapper outputDataMapper; + + @Mock + private OutputDataService outputDataServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restOutputDataMockMvc; + + private OutputData outputData; + + private OutputData insertedOutputData; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static OutputData createEntity(EntityManager em) { + OutputData outputData = new OutputData().value(DEFAULT_VALUE).version(DEFAULT_VERSION); + return outputData; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static OutputData createUpdatedEntity(EntityManager em) { + OutputData outputData = new OutputData().value(UPDATED_VALUE).version(UPDATED_VERSION); + return outputData; + } + + @BeforeEach + public void initTest() { + outputData = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedOutputData != null) { + outputDataRepository.delete(insertedOutputData); + insertedOutputData = null; + } + } + + @Test + @Transactional + void createOutputData() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the OutputData + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + var returnedOutputDataDTO = om.readValue( + restOutputDataMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(outputDataDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + OutputDataDTO.class + ); + + // Validate the OutputData in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedOutputData = outputDataMapper.toEntity(returnedOutputDataDTO); + assertOutputDataUpdatableFieldsEquals(returnedOutputData, getPersistedOutputData(returnedOutputData)); + + insertedOutputData = returnedOutputData; + } + + @Test + @Transactional + void createOutputDataWithExistingId() throws Exception { + // Create the OutputData with an existing ID + outputData.setId(1L); + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restOutputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(outputDataDTO))) + .andExpect(status().isBadRequest()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkValueIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + outputData.setValue(null); + + // Create the OutputData, which fails. + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + restOutputDataMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(outputDataDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllOutputData() throws Exception { + // Initialize the database + insertedOutputData = outputDataRepository.saveAndFlush(outputData); + + // Get all the outputDataList + restOutputDataMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(outputData.getId().intValue()))) + .andExpect(jsonPath("$.[*].value").value(hasItem(sameNumber(DEFAULT_VALUE)))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllOutputDataWithEagerRelationshipsIsEnabled() throws Exception { + when(outputDataServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restOutputDataMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(outputDataServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllOutputDataWithEagerRelationshipsIsNotEnabled() throws Exception { + when(outputDataServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restOutputDataMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(outputDataRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getOutputData() throws Exception { + // Initialize the database + insertedOutputData = outputDataRepository.saveAndFlush(outputData); + + // Get the outputData + restOutputDataMockMvc + .perform(get(ENTITY_API_URL_ID, outputData.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(outputData.getId().intValue())) + .andExpect(jsonPath("$.value").value(sameNumber(DEFAULT_VALUE))) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingOutputData() throws Exception { + // Get the outputData + restOutputDataMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingOutputData() throws Exception { + // Initialize the database + insertedOutputData = outputDataRepository.saveAndFlush(outputData); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the outputData + OutputData updatedOutputData = outputDataRepository.findById(outputData.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedOutputData are not directly saved in db + em.detach(updatedOutputData); + updatedOutputData.value(UPDATED_VALUE).version(UPDATED_VERSION); + OutputDataDTO outputDataDTO = outputDataMapper.toDto(updatedOutputData); + + restOutputDataMockMvc + .perform( + put(ENTITY_API_URL_ID, outputDataDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(outputDataDTO)) + ) + .andExpect(status().isOk()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedOutputDataToMatchAllProperties(updatedOutputData); + } + + @Test + @Transactional + void putNonExistingOutputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + outputData.setId(longCount.incrementAndGet()); + + // Create the OutputData + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restOutputDataMockMvc + .perform( + put(ENTITY_API_URL_ID, outputDataDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(outputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchOutputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + outputData.setId(longCount.incrementAndGet()); + + // Create the OutputData + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOutputDataMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(outputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamOutputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + outputData.setId(longCount.incrementAndGet()); + + // Create the OutputData + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOutputDataMockMvc + .perform(put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(outputDataDTO))) + .andExpect(status().isMethodNotAllowed()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateOutputDataWithPatch() throws Exception { + // Initialize the database + insertedOutputData = outputDataRepository.saveAndFlush(outputData); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the outputData using partial update + OutputData partialUpdatedOutputData = new OutputData(); + partialUpdatedOutputData.setId(outputData.getId()); + + restOutputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedOutputData.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedOutputData)) + ) + .andExpect(status().isOk()); + + // Validate the OutputData in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertOutputDataUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedOutputData, outputData), + getPersistedOutputData(outputData) + ); + } + + @Test + @Transactional + void fullUpdateOutputDataWithPatch() throws Exception { + // Initialize the database + insertedOutputData = outputDataRepository.saveAndFlush(outputData); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the outputData using partial update + OutputData partialUpdatedOutputData = new OutputData(); + partialUpdatedOutputData.setId(outputData.getId()); + + partialUpdatedOutputData.value(UPDATED_VALUE).version(UPDATED_VERSION); + + restOutputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedOutputData.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedOutputData)) + ) + .andExpect(status().isOk()); + + // Validate the OutputData in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertOutputDataUpdatableFieldsEquals(partialUpdatedOutputData, getPersistedOutputData(partialUpdatedOutputData)); + } + + @Test + @Transactional + void patchNonExistingOutputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + outputData.setId(longCount.incrementAndGet()); + + // Create the OutputData + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restOutputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, outputDataDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(outputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchOutputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + outputData.setId(longCount.incrementAndGet()); + + // Create the OutputData + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOutputDataMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(outputDataDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamOutputData() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + outputData.setId(longCount.incrementAndGet()); + + // Create the OutputData + OutputDataDTO outputDataDTO = outputDataMapper.toDto(outputData); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restOutputDataMockMvc + .perform( + patch(ENTITY_API_URL).with(csrf()).contentType("application/merge-patch+json").content(om.writeValueAsBytes(outputDataDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the OutputData in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteOutputData() throws Exception { + // Initialize the database + insertedOutputData = outputDataRepository.saveAndFlush(outputData); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the outputData + restOutputDataMockMvc + .perform(delete(ENTITY_API_URL_ID, outputData.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return outputDataRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected OutputData getPersistedOutputData(OutputData outputData) { + return outputDataRepository.findById(outputData.getId()).orElseThrow(); + } + + protected void assertPersistedOutputDataToMatchAllProperties(OutputData expectedOutputData) { + assertOutputDataAllPropertiesEquals(expectedOutputData, getPersistedOutputData(expectedOutputData)); + } + + protected void assertPersistedOutputDataToMatchUpdatableProperties(OutputData expectedOutputData) { + assertOutputDataAllUpdatablePropertiesEquals(expectedOutputData, getPersistedOutputData(expectedOutputData)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/PeriodResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/PeriodResourceIT.java new file mode 100644 index 0000000..7c2fd01 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/PeriodResourceIT.java @@ -0,0 +1,620 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.PeriodAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.Period; +import com.oguerreiro.resilient.domain.enumeration.PeriodStatus; +import com.oguerreiro.resilient.repository.PeriodRepository; +import com.oguerreiro.resilient.service.dto.PeriodDTO; +import com.oguerreiro.resilient.service.mapper.PeriodMapper; +import jakarta.persistence.EntityManager; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link PeriodResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +@WithMockUser +class PeriodResourceIT { + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final LocalDate DEFAULT_BEGIN_DATE = LocalDate.ofEpochDay(0L); + private static final LocalDate UPDATED_BEGIN_DATE = LocalDate.now(ZoneId.systemDefault()); + + private static final LocalDate DEFAULT_END_DATE = LocalDate.ofEpochDay(0L); + private static final LocalDate UPDATED_END_DATE = LocalDate.now(ZoneId.systemDefault()); + + private static final PeriodStatus DEFAULT_STATE = PeriodStatus.CREATED; + private static final PeriodStatus UPDATED_STATE = PeriodStatus.OPENED; + + private static final Instant DEFAULT_CREATION_DATE = Instant.ofEpochMilli(0L); + private static final Instant UPDATED_CREATION_DATE = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + private static final String DEFAULT_CREATION_USERNAME = "AAAAAAAAAA"; + private static final String UPDATED_CREATION_USERNAME = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/periods"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private PeriodRepository periodRepository; + + @Autowired + private PeriodMapper periodMapper; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restPeriodMockMvc; + + private Period period; + + private Period insertedPeriod; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Period createEntity(EntityManager em) { + Period period = new Period() + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .beginDate(DEFAULT_BEGIN_DATE) + .endDate(DEFAULT_END_DATE) + .state(DEFAULT_STATE) + .creationDate(DEFAULT_CREATION_DATE) + .creationUsername(DEFAULT_CREATION_USERNAME) + .version(DEFAULT_VERSION); + return period; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Period createUpdatedEntity(EntityManager em) { + Period period = new Period() + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .beginDate(UPDATED_BEGIN_DATE) + .endDate(UPDATED_END_DATE) + .state(UPDATED_STATE) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + return period; + } + + @BeforeEach + public void initTest() { + period = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedPeriod != null) { + periodRepository.delete(insertedPeriod); + insertedPeriod = null; + } + } + + @Test + @Transactional + void createPeriod() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the Period + PeriodDTO periodDTO = periodMapper.toDto(period); + var returnedPeriodDTO = om.readValue( + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + PeriodDTO.class + ); + + // Validate the Period in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedPeriod = periodMapper.toEntity(returnedPeriodDTO); + assertPeriodUpdatableFieldsEquals(returnedPeriod, getPersistedPeriod(returnedPeriod)); + + insertedPeriod = returnedPeriod; + } + + @Test + @Transactional + void createPeriodWithExistingId() throws Exception { + // Create the Period with an existing ID + period.setId(1L); + PeriodDTO periodDTO = periodMapper.toDto(period); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isBadRequest()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + period.setName(null); + + // Create the Period, which fails. + PeriodDTO periodDTO = periodMapper.toDto(period); + + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkBeginDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + period.setBeginDate(null); + + // Create the Period, which fails. + PeriodDTO periodDTO = periodMapper.toDto(period); + + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkEndDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + period.setEndDate(null); + + // Create the Period, which fails. + PeriodDTO periodDTO = periodMapper.toDto(period); + + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkStateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + period.setState(null); + + // Create the Period, which fails. + PeriodDTO periodDTO = periodMapper.toDto(period); + + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + period.setCreationDate(null); + + // Create the Period, which fails. + PeriodDTO periodDTO = periodMapper.toDto(period); + + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationUsernameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + period.setCreationUsername(null); + + // Create the Period, which fails. + PeriodDTO periodDTO = periodMapper.toDto(period); + + restPeriodMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllPeriods() throws Exception { + // Initialize the database + insertedPeriod = periodRepository.saveAndFlush(period); + + // Get all the periodList + restPeriodMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(period.getId().intValue()))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].beginDate").value(hasItem(DEFAULT_BEGIN_DATE.toString()))) + .andExpect(jsonPath("$.[*].endDate").value(hasItem(DEFAULT_END_DATE.toString()))) + .andExpect(jsonPath("$.[*].state").value(hasItem(DEFAULT_STATE.toString()))) + .andExpect(jsonPath("$.[*].creationDate").value(hasItem(DEFAULT_CREATION_DATE.toString()))) + .andExpect(jsonPath("$.[*].creationUsername").value(hasItem(DEFAULT_CREATION_USERNAME))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @Test + @Transactional + void getPeriod() throws Exception { + // Initialize the database + insertedPeriod = periodRepository.saveAndFlush(period); + + // Get the period + restPeriodMockMvc + .perform(get(ENTITY_API_URL_ID, period.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(period.getId().intValue())) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.beginDate").value(DEFAULT_BEGIN_DATE.toString())) + .andExpect(jsonPath("$.endDate").value(DEFAULT_END_DATE.toString())) + .andExpect(jsonPath("$.state").value(DEFAULT_STATE.toString())) + .andExpect(jsonPath("$.creationDate").value(DEFAULT_CREATION_DATE.toString())) + .andExpect(jsonPath("$.creationUsername").value(DEFAULT_CREATION_USERNAME)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingPeriod() throws Exception { + // Get the period + restPeriodMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingPeriod() throws Exception { + // Initialize the database + insertedPeriod = periodRepository.saveAndFlush(period); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the period + Period updatedPeriod = periodRepository.findById(period.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedPeriod are not directly saved in db + em.detach(updatedPeriod); + updatedPeriod + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .beginDate(UPDATED_BEGIN_DATE) + .endDate(UPDATED_END_DATE) + .state(UPDATED_STATE) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + PeriodDTO periodDTO = periodMapper.toDto(updatedPeriod); + + restPeriodMockMvc + .perform( + put(ENTITY_API_URL_ID, periodDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodDTO)) + ) + .andExpect(status().isOk()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedPeriodToMatchAllProperties(updatedPeriod); + } + + @Test + @Transactional + void putNonExistingPeriod() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + period.setId(longCount.incrementAndGet()); + + // Create the Period + PeriodDTO periodDTO = periodMapper.toDto(period); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restPeriodMockMvc + .perform( + put(ENTITY_API_URL_ID, periodDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchPeriod() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + period.setId(longCount.incrementAndGet()); + + // Create the Period + PeriodDTO periodDTO = periodMapper.toDto(period); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamPeriod() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + period.setId(longCount.incrementAndGet()); + + // Create the Period + PeriodDTO periodDTO = periodMapper.toDto(period); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodMockMvc + .perform(put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodDTO))) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdatePeriodWithPatch() throws Exception { + // Initialize the database + insertedPeriod = periodRepository.saveAndFlush(period); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the period using partial update + Period partialUpdatedPeriod = new Period(); + partialUpdatedPeriod.setId(period.getId()); + + partialUpdatedPeriod + .name(UPDATED_NAME) + .beginDate(UPDATED_BEGIN_DATE) + .state(UPDATED_STATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + + restPeriodMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedPeriod.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedPeriod)) + ) + .andExpect(status().isOk()); + + // Validate the Period in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPeriodUpdatableFieldsEquals(createUpdateProxyForBean(partialUpdatedPeriod, period), getPersistedPeriod(period)); + } + + @Test + @Transactional + void fullUpdatePeriodWithPatch() throws Exception { + // Initialize the database + insertedPeriod = periodRepository.saveAndFlush(period); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the period using partial update + Period partialUpdatedPeriod = new Period(); + partialUpdatedPeriod.setId(period.getId()); + + partialUpdatedPeriod + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .beginDate(UPDATED_BEGIN_DATE) + .endDate(UPDATED_END_DATE) + .state(UPDATED_STATE) + .creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + + restPeriodMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedPeriod.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedPeriod)) + ) + .andExpect(status().isOk()); + + // Validate the Period in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPeriodUpdatableFieldsEquals(partialUpdatedPeriod, getPersistedPeriod(partialUpdatedPeriod)); + } + + @Test + @Transactional + void patchNonExistingPeriod() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + period.setId(longCount.incrementAndGet()); + + // Create the Period + PeriodDTO periodDTO = periodMapper.toDto(period); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restPeriodMockMvc + .perform( + patch(ENTITY_API_URL_ID, periodDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(periodDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchPeriod() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + period.setId(longCount.incrementAndGet()); + + // Create the Period + PeriodDTO periodDTO = periodMapper.toDto(period); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(periodDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamPeriod() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + period.setId(longCount.incrementAndGet()); + + // Create the Period + PeriodDTO periodDTO = periodMapper.toDto(period); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodMockMvc + .perform( + patch(ENTITY_API_URL).with(csrf()).contentType("application/merge-patch+json").content(om.writeValueAsBytes(periodDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Period in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deletePeriod() throws Exception { + // Initialize the database + insertedPeriod = periodRepository.saveAndFlush(period); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the period + restPeriodMockMvc + .perform(delete(ENTITY_API_URL_ID, period.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return periodRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected Period getPersistedPeriod(Period period) { + return periodRepository.findById(period.getId()).orElseThrow(); + } + + protected void assertPersistedPeriodToMatchAllProperties(Period expectedPeriod) { + assertPeriodAllPropertiesEquals(expectedPeriod, getPersistedPeriod(expectedPeriod)); + } + + protected void assertPersistedPeriodToMatchUpdatableProperties(Period expectedPeriod) { + assertPeriodAllUpdatablePropertiesEquals(expectedPeriod, getPersistedPeriod(expectedPeriod)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/PeriodVersionResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/PeriodVersionResourceIT.java new file mode 100644 index 0000000..d2fc17e --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/PeriodVersionResourceIT.java @@ -0,0 +1,568 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.PeriodVersionAsserts.assertPeriodVersionAllPropertiesEquals; +import static com.oguerreiro.resilient.domain.PeriodVersionAsserts.assertPeriodVersionAllUpdatablePropertiesEquals; +import static com.oguerreiro.resilient.domain.PeriodVersionAsserts.assertPeriodVersionUpdatableFieldsEquals; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.PeriodVersion; +import com.oguerreiro.resilient.domain.enumeration.PeriodVersionStatus; +import com.oguerreiro.resilient.repository.PeriodVersionRepository; +import com.oguerreiro.resilient.service.PeriodVersionService; +import com.oguerreiro.resilient.service.dto.PeriodVersionDTO; +import com.oguerreiro.resilient.service.mapper.PeriodVersionMapper; + +import jakarta.persistence.EntityManager; + +/** + * Integration tests for the {@link PeriodVersionResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class PeriodVersionResourceIT { + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final Integer DEFAULT_PERIOD_VERSION = 1; + private static final Integer UPDATED_PERIOD_VERSION = 2; + + private static final PeriodVersionStatus DEFAULT_STATE = PeriodVersionStatus.PROCESSING; + private static final PeriodVersionStatus UPDATED_STATE = PeriodVersionStatus.ENDED; + + private static final Instant DEFAULT_CREATION_DATE = Instant.ofEpochMilli(0L); + private static final Instant UPDATED_CREATION_DATE = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + private static final String DEFAULT_CREATION_USERNAME = "AAAAAAAAAA"; + private static final String UPDATED_CREATION_USERNAME = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/period-versions"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private PeriodVersionRepository periodVersionRepository; + + @Mock + private PeriodVersionRepository periodVersionRepositoryMock; + + @Autowired + private PeriodVersionMapper periodVersionMapper; + + @Mock + private PeriodVersionService periodVersionServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restPeriodVersionMockMvc; + + private PeriodVersion periodVersion; + + private PeriodVersion insertedPeriodVersion; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, if + * they test an entity which requires the current entity. + */ + public static PeriodVersion createEntity(EntityManager em) { + PeriodVersion periodVersion = new PeriodVersion().name(DEFAULT_NAME).description(DEFAULT_DESCRIPTION) + .periodVersion(DEFAULT_PERIOD_VERSION).state(DEFAULT_STATE).creationDate(DEFAULT_CREATION_DATE) + .creationUsername(DEFAULT_CREATION_USERNAME).version(DEFAULT_VERSION); + return periodVersion; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, if + * they test an entity which requires the current entity. + */ + public static PeriodVersion createUpdatedEntity(EntityManager em) { + PeriodVersion periodVersion = new PeriodVersion().name(UPDATED_NAME).description(UPDATED_DESCRIPTION) + .periodVersion(UPDATED_PERIOD_VERSION).state(UPDATED_STATE).creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME).version(UPDATED_VERSION); + return periodVersion; + } + + @BeforeEach + public void initTest() { + periodVersion = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedPeriodVersion != null) { + periodVersionRepository.delete(insertedPeriodVersion); + insertedPeriodVersion = null; + } + } + + @Test + @Transactional + void createPeriodVersion() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the PeriodVersion + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + var returnedPeriodVersionDTO = om.readValue(restPeriodVersionMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))) + .andExpect(status().isCreated()).andReturn().getResponse().getContentAsString(), PeriodVersionDTO.class); + + // Validate the PeriodVersion in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedPeriodVersion = periodVersionMapper.toEntity(returnedPeriodVersionDTO); + assertPeriodVersionUpdatableFieldsEquals(returnedPeriodVersion, getPersistedPeriodVersion(returnedPeriodVersion)); + + insertedPeriodVersion = returnedPeriodVersion; + } + + @Test + @Transactional + void createPeriodVersionWithExistingId() throws Exception { + // Create the PeriodVersion with an existing ID + periodVersion.setId(1L); + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restPeriodVersionMockMvc.perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isBadRequest()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + periodVersion.setName(null); + + // Create the PeriodVersion, which fails. + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + restPeriodVersionMockMvc.perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkPeriodVersionIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + periodVersion.setPeriodVersion(null); + + // Create the PeriodVersion, which fails. + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + restPeriodVersionMockMvc.perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkStateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + periodVersion.setState(null); + + // Create the PeriodVersion, which fails. + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + restPeriodVersionMockMvc.perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationDateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + periodVersion.setCreationDate(null); + + // Create the PeriodVersion, which fails. + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + restPeriodVersionMockMvc.perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkCreationUsernameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + periodVersion.setCreationUsername(null); + + // Create the PeriodVersion, which fails. + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + restPeriodVersionMockMvc.perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllPeriodVersions() throws Exception { + // Initialize the database + insertedPeriodVersion = periodVersionRepository.saveAndFlush(periodVersion); + + // Get all the periodVersionList + restPeriodVersionMockMvc.perform(get(ENTITY_API_URL + "?sort=id,desc")).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(periodVersion.getId().intValue()))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].periodVersion").value(hasItem(DEFAULT_PERIOD_VERSION))) + .andExpect(jsonPath("$.[*].state").value(hasItem(DEFAULT_STATE.toString()))) + .andExpect(jsonPath("$.[*].creationDate").value(hasItem(DEFAULT_CREATION_DATE.toString()))) + .andExpect(jsonPath("$.[*].creationUsername").value(hasItem(DEFAULT_CREATION_USERNAME))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllPeriodVersionsWithEagerRelationshipsIsEnabled() throws Exception { + when(periodVersionServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restPeriodVersionMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(periodVersionServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllPeriodVersionsWithEagerRelationshipsIsNotEnabled() throws Exception { + when(periodVersionServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restPeriodVersionMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(periodVersionRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getPeriodVersion() throws Exception { + // Initialize the database + insertedPeriodVersion = periodVersionRepository.saveAndFlush(periodVersion); + + // Get the periodVersion + restPeriodVersionMockMvc.perform(get(ENTITY_API_URL_ID, periodVersion.getId())).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(periodVersion.getId().intValue())) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.periodVersion").value(DEFAULT_PERIOD_VERSION)) + .andExpect(jsonPath("$.state").value(DEFAULT_STATE.toString())) + .andExpect(jsonPath("$.creationDate").value(DEFAULT_CREATION_DATE.toString())) + .andExpect(jsonPath("$.creationUsername").value(DEFAULT_CREATION_USERNAME)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingPeriodVersion() throws Exception { + // Get the periodVersion + restPeriodVersionMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingPeriodVersion() throws Exception { + // Initialize the database + insertedPeriodVersion = periodVersionRepository.saveAndFlush(periodVersion); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the periodVersion + PeriodVersion updatedPeriodVersion = periodVersionRepository.findById(periodVersion.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedPeriodVersion are not + // directly saved in db + em.detach(updatedPeriodVersion); + updatedPeriodVersion.name(UPDATED_NAME).description(UPDATED_DESCRIPTION).periodVersion(UPDATED_PERIOD_VERSION) + .state(UPDATED_STATE).creationDate(UPDATED_CREATION_DATE).creationUsername(UPDATED_CREATION_USERNAME) + .version(UPDATED_VERSION); + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(updatedPeriodVersion); + + restPeriodVersionMockMvc.perform(put(ENTITY_API_URL_ID, periodVersionDTO.getId()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodVersionDTO))) + .andExpect(status().isOk()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedPeriodVersionToMatchAllProperties(updatedPeriodVersion); + } + + @Test + @Transactional + void putNonExistingPeriodVersion() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + periodVersion.setId(longCount.incrementAndGet()); + + // Create the PeriodVersion + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restPeriodVersionMockMvc.perform(put(ENTITY_API_URL_ID, periodVersionDTO.getId()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodVersionDTO))) + .andExpect(status().isBadRequest()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchPeriodVersion() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + periodVersion.setId(longCount.incrementAndGet()); + + // Create the PeriodVersion + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodVersionMockMvc + .perform(put(ENTITY_API_URL_ID, longCount.incrementAndGet()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(periodVersionDTO))) + .andExpect(status().isBadRequest()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamPeriodVersion() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + periodVersion.setId(longCount.incrementAndGet()); + + // Create the PeriodVersion + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodVersionMockMvc.perform(put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isMethodNotAllowed()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdatePeriodVersionWithPatch() throws Exception { + // Initialize the database + insertedPeriodVersion = periodVersionRepository.saveAndFlush(periodVersion); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the periodVersion using partial update + PeriodVersion partialUpdatedPeriodVersion = new PeriodVersion(); + partialUpdatedPeriodVersion.setId(periodVersion.getId()); + + partialUpdatedPeriodVersion.description(UPDATED_DESCRIPTION).state(UPDATED_STATE); + + restPeriodVersionMockMvc + .perform(patch(ENTITY_API_URL_ID, partialUpdatedPeriodVersion.getId()).with(csrf()) + .contentType("application/merge-patch+json").content(om.writeValueAsBytes(partialUpdatedPeriodVersion))) + .andExpect(status().isOk()); + + // Validate the PeriodVersion in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPeriodVersionUpdatableFieldsEquals(createUpdateProxyForBean(partialUpdatedPeriodVersion, periodVersion), + getPersistedPeriodVersion(periodVersion)); + } + + @Test + @Transactional + void fullUpdatePeriodVersionWithPatch() throws Exception { + // Initialize the database + insertedPeriodVersion = periodVersionRepository.saveAndFlush(periodVersion); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the periodVersion using partial update + PeriodVersion partialUpdatedPeriodVersion = new PeriodVersion(); + partialUpdatedPeriodVersion.setId(periodVersion.getId()); + + partialUpdatedPeriodVersion.name(UPDATED_NAME).description(UPDATED_DESCRIPTION) + .periodVersion(UPDATED_PERIOD_VERSION).state(UPDATED_STATE).creationDate(UPDATED_CREATION_DATE) + .creationUsername(UPDATED_CREATION_USERNAME).version(UPDATED_VERSION); + + restPeriodVersionMockMvc + .perform(patch(ENTITY_API_URL_ID, partialUpdatedPeriodVersion.getId()).with(csrf()) + .contentType("application/merge-patch+json").content(om.writeValueAsBytes(partialUpdatedPeriodVersion))) + .andExpect(status().isOk()); + + // Validate the PeriodVersion in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPeriodVersionUpdatableFieldsEquals(partialUpdatedPeriodVersion, + getPersistedPeriodVersion(partialUpdatedPeriodVersion)); + } + + @Test + @Transactional + void patchNonExistingPeriodVersion() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + periodVersion.setId(longCount.incrementAndGet()); + + // Create the PeriodVersion + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restPeriodVersionMockMvc + .perform(patch(ENTITY_API_URL_ID, periodVersionDTO.getId()).with(csrf()) + .contentType("application/merge-patch+json").content(om.writeValueAsBytes(periodVersionDTO))) + .andExpect(status().isBadRequest()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchPeriodVersion() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + periodVersion.setId(longCount.incrementAndGet()); + + // Create the PeriodVersion + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodVersionMockMvc + .perform(patch(ENTITY_API_URL_ID, longCount.incrementAndGet()).with(csrf()) + .contentType("application/merge-patch+json").content(om.writeValueAsBytes(periodVersionDTO))) + .andExpect(status().isBadRequest()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamPeriodVersion() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + periodVersion.setId(longCount.incrementAndGet()); + + // Create the PeriodVersion + PeriodVersionDTO periodVersionDTO = periodVersionMapper.toDto(periodVersion); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restPeriodVersionMockMvc.perform(patch(ENTITY_API_URL).with(csrf()).contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(periodVersionDTO))).andExpect(status().isMethodNotAllowed()); + + // Validate the PeriodVersion in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deletePeriodVersion() throws Exception { + // Initialize the database + insertedPeriodVersion = periodVersionRepository.saveAndFlush(periodVersion); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the periodVersion + restPeriodVersionMockMvc + .perform(delete(ENTITY_API_URL_ID, periodVersion.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return periodVersionRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected PeriodVersion getPersistedPeriodVersion(PeriodVersion periodVersion) { + return periodVersionRepository.findById(periodVersion.getId()).orElseThrow(); + } + + protected void assertPersistedPeriodVersionToMatchAllProperties(PeriodVersion expectedPeriodVersion) { + assertPeriodVersionAllPropertiesEquals(expectedPeriodVersion, getPersistedPeriodVersion(expectedPeriodVersion)); + } + + protected void assertPersistedPeriodVersionToMatchUpdatableProperties(PeriodVersion expectedPeriodVersion) { + assertPeriodVersionAllUpdatablePropertiesEquals(expectedPeriodVersion, + getPersistedPeriodVersion(expectedPeriodVersion)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/PublicUserResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/PublicUserResourceIT.java new file mode 100644 index 0000000..7986e75 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/PublicUserResourceIT.java @@ -0,0 +1,109 @@ +package com.oguerreiro.resilient.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.service.UserService; +import jakarta.persistence.EntityManager; +import java.util.Objects; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link PublicUserResource} REST controller. + */ +@AutoConfigureMockMvc +@WithMockUser(authorities = AuthoritiesConstants.ADMIN) +@IntegrationTest +class PublicUserResourceIT { + + private static final String DEFAULT_LOGIN = "johndoe"; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + private EntityManager em; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private MockMvc restUserMockMvc; + + private User user; + private Long numberOfUsers; + + @BeforeEach + public void countUsers() { + numberOfUsers = userRepository.count(); + } + + @BeforeEach + public void initTest() { + user = UserResourceIT.initTestUser(em); + } + + @AfterEach + public void cleanupAndCheck() { + cacheManager + .getCacheNames() + .stream() + .map(cacheName -> this.cacheManager.getCache(cacheName)) + .filter(Objects::nonNull) + .forEach(Cache::clear); + userService.deleteUser(user.getLogin()); + assertThat(userRepository.count()).isEqualTo(numberOfUsers); + numberOfUsers = null; + } + + @Test + @Transactional + void getAllPublicUsers() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + // Get all the users + restUserMockMvc + .perform(get("/api/users?sort=id,desc").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[?(@.id == %d)].login", user.getId()).value(user.getLogin())) + .andExpect(jsonPath("$.[?(@.id == %d)].keys()", user.getId()).value(Set.of("id", "login"))) + .andExpect(jsonPath("$.[*].email").doesNotHaveJsonPath()) + .andExpect(jsonPath("$.[*].imageUrl").doesNotHaveJsonPath()) + .andExpect(jsonPath("$.[*].langKey").doesNotHaveJsonPath()); + } + + @Test + @Transactional + void getAllUsersSortedByParameters() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + restUserMockMvc.perform(get("/api/users?sort=resetKey,desc").accept(MediaType.APPLICATION_JSON)).andExpect(status().isBadRequest()); + restUserMockMvc.perform(get("/api/users?sort=password,desc").accept(MediaType.APPLICATION_JSON)).andExpect(status().isBadRequest()); + restUserMockMvc + .perform(get("/api/users?sort=resetKey,desc&sort=id,desc").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + restUserMockMvc.perform(get("/api/users?sort=id,desc").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/TestUtil.java b/src/test/java/com/oguerreiro/resilient/web/rest/TestUtil.java new file mode 100644 index 0000000..b6efa86 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/TestUtil.java @@ -0,0 +1,202 @@ +package com.oguerreiro.resilient.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.List; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.hamcrest.TypeSafeMatcher; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; + +/** + * Utility class for testing REST controllers. + */ +public final class TestUtil { + + /** + * Create a byte array with a specific size filled with specified data. + * + * @param size the size of the byte array. + * @param data the data to put in the byte array. + * @return the JSON byte array. + */ + public static byte[] createByteArray(int size, String data) { + byte[] byteArray = new byte[size]; + for (int i = 0; i < size; i++) { + byteArray[i] = Byte.parseByte(data, 2); + } + return byteArray; + } + + /** + * A matcher that tests that the examined string represents the same instant as the reference datetime. + */ + public static class ZonedDateTimeMatcher extends TypeSafeDiagnosingMatcher { + + private final ZonedDateTime date; + + public ZonedDateTimeMatcher(ZonedDateTime date) { + this.date = date; + } + + @Override + protected boolean matchesSafely(String item, Description mismatchDescription) { + try { + if (!date.isEqual(ZonedDateTime.parse(item))) { + mismatchDescription.appendText("was ").appendValue(item); + return false; + } + return true; + } catch (DateTimeParseException e) { + mismatchDescription.appendText("was ").appendValue(item).appendText(", which could not be parsed as a ZonedDateTime"); + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("a String representing the same Instant as ").appendValue(date); + } + } + + /** + * Creates a matcher that matches when the examined string represents the same instant as the reference datetime. + * + * @param date the reference datetime against which the examined string is checked. + */ + public static ZonedDateTimeMatcher sameInstant(ZonedDateTime date) { + return new ZonedDateTimeMatcher(date); + } + + /** + * A matcher that tests that the examined number represents the same value - it can be Long, Double, etc - as the reference BigDecimal. + */ + public static class NumberMatcher extends TypeSafeMatcher { + + final BigDecimal value; + + public NumberMatcher(BigDecimal value) { + this.value = value; + } + + @Override + public void describeTo(Description description) { + description.appendText("a numeric value is ").appendValue(value); + } + + @Override + protected boolean matchesSafely(Number item) { + BigDecimal bigDecimal = asDecimal(item); + return bigDecimal != null && value.compareTo(bigDecimal) == 0; + } + + private static BigDecimal asDecimal(Number item) { + if (item == null) { + return null; + } + if (item instanceof BigDecimal) { + return (BigDecimal) item; + } else if (item instanceof Long) { + return BigDecimal.valueOf((Long) item); + } else if (item instanceof Integer) { + return BigDecimal.valueOf((Integer) item); + } else if (item instanceof Double) { + return BigDecimal.valueOf((Double) item); + } else if (item instanceof Float) { + return BigDecimal.valueOf((Float) item); + } else { + return BigDecimal.valueOf(item.doubleValue()); + } + } + } + + /** + * Creates a matcher that matches when the examined number represents the same value as the reference BigDecimal. + * + * @param number the reference BigDecimal against which the examined number is checked. + */ + public static NumberMatcher sameNumber(BigDecimal number) { + return new NumberMatcher(number); + } + + /** + * Verifies the equals/hashcode contract on the domain object. + */ + public static void equalsVerifier(Class clazz) throws Exception { + T domainObject1 = clazz.getConstructor().newInstance(); + assertThat(domainObject1.toString()).isNotNull(); + assertThat(domainObject1).isEqualTo(domainObject1); + assertThat(domainObject1).hasSameHashCodeAs(domainObject1); + // Test with an instance of another class + Object testOtherObject = new Object(); + assertThat(domainObject1).isNotEqualTo(testOtherObject); + assertThat(domainObject1).isNotEqualTo(null); + // Test with an instance of the same class + T domainObject2 = clazz.getConstructor().newInstance(); + assertThat(domainObject1).isNotEqualTo(domainObject2); + // HashCodes are equals because the objects are not persisted yet + assertThat(domainObject1).hasSameHashCodeAs(domainObject2); + } + + /** + * Create a {@link FormattingConversionService} which use ISO date format, instead of the localized one. + * @return the {@link FormattingConversionService}. + */ + public static FormattingConversionService createFormattingConversionService() { + DefaultFormattingConversionService dfcs = new DefaultFormattingConversionService(); + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(dfcs); + return dfcs; + } + + /** + * Executes a query on the EntityManager finding all stored objects. + * @param The type of objects to be searched + * @param em The instance of the EntityManager + * @param clazz The class type to be searched + * @return A list of all found objects + */ + public static List findAll(EntityManager em, Class clazz) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(clazz); + Root rootEntry = cq.from(clazz); + CriteriaQuery all = cq.select(rootEntry); + TypedQuery allQuery = em.createQuery(all); + return allQuery.getResultList(); + } + + @SuppressWarnings("unchecked") + public static T createUpdateProxyForBean(T update, T original) { + Enhancer e = new Enhancer(); + e.setSuperclass(original.getClass()); + e.setCallback( + new MethodInterceptor() { + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + Object val = update.getClass().getMethod(method.getName(), method.getParameterTypes()).invoke(update, args); + if (val == null) { + return original.getClass().getMethod(method.getName(), method.getParameterTypes()).invoke(original, args); + } + return val; + } + } + ); + return (T) e.create(); + } + + private TestUtil() {} +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/UnitConverterResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/UnitConverterResourceIT.java new file mode 100644 index 0000000..86276b6 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/UnitConverterResourceIT.java @@ -0,0 +1,576 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.UnitConverterAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.UnitConverter; +import com.oguerreiro.resilient.repository.UnitConverterRepository; +import com.oguerreiro.resilient.service.UnitConverterService; +import com.oguerreiro.resilient.service.dto.UnitConverterDTO; +import com.oguerreiro.resilient.service.mapper.UnitConverterMapper; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link UnitConverterResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class UnitConverterResourceIT { + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final String DEFAULT_CONVERTION_FORMULA = "AAAAAAAAAA"; + private static final String UPDATED_CONVERTION_FORMULA = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/unit-converters"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private UnitConverterRepository unitConverterRepository; + + @Mock + private UnitConverterRepository unitConverterRepositoryMock; + + @Autowired + private UnitConverterMapper unitConverterMapper; + + @Mock + private UnitConverterService unitConverterServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restUnitConverterMockMvc; + + private UnitConverter unitConverter; + + private UnitConverter insertedUnitConverter; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static UnitConverter createEntity(EntityManager em) { + UnitConverter unitConverter = new UnitConverter() + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .convertionFormula(DEFAULT_CONVERTION_FORMULA) + .version(DEFAULT_VERSION); + return unitConverter; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static UnitConverter createUpdatedEntity(EntityManager em) { + UnitConverter unitConverter = new UnitConverter() + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .convertionFormula(UPDATED_CONVERTION_FORMULA) + .version(UPDATED_VERSION); + return unitConverter; + } + + @BeforeEach + public void initTest() { + unitConverter = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedUnitConverter != null) { + unitConverterRepository.delete(insertedUnitConverter); + insertedUnitConverter = null; + } + } + + @Test + @Transactional + void createUnitConverter() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the UnitConverter + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + var returnedUnitConverterDTO = om.readValue( + restUnitConverterMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + UnitConverterDTO.class + ); + + // Validate the UnitConverter in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedUnitConverter = unitConverterMapper.toEntity(returnedUnitConverterDTO); + assertUnitConverterUpdatableFieldsEquals(returnedUnitConverter, getPersistedUnitConverter(returnedUnitConverter)); + + insertedUnitConverter = returnedUnitConverter; + } + + @Test + @Transactional + void createUnitConverterWithExistingId() throws Exception { + // Create the UnitConverter with an existing ID + unitConverter.setId(1L); + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restUnitConverterMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unitConverter.setName(null); + + // Create the UnitConverter, which fails. + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + restUnitConverterMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkDescriptionIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unitConverter.setDescription(null); + + // Create the UnitConverter, which fails. + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + restUnitConverterMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkConvertionFormulaIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unitConverter.setConvertionFormula(null); + + // Create the UnitConverter, which fails. + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + restUnitConverterMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllUnitConverters() throws Exception { + // Initialize the database + insertedUnitConverter = unitConverterRepository.saveAndFlush(unitConverter); + + // Get all the unitConverterList + restUnitConverterMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(unitConverter.getId().intValue()))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].convertionFormula").value(hasItem(DEFAULT_CONVERTION_FORMULA))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllUnitConvertersWithEagerRelationshipsIsEnabled() throws Exception { + when(unitConverterServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restUnitConverterMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(unitConverterServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllUnitConvertersWithEagerRelationshipsIsNotEnabled() throws Exception { + when(unitConverterServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restUnitConverterMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(unitConverterRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getUnitConverter() throws Exception { + // Initialize the database + insertedUnitConverter = unitConverterRepository.saveAndFlush(unitConverter); + + // Get the unitConverter + restUnitConverterMockMvc + .perform(get(ENTITY_API_URL_ID, unitConverter.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(unitConverter.getId().intValue())) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.convertionFormula").value(DEFAULT_CONVERTION_FORMULA)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingUnitConverter() throws Exception { + // Get the unitConverter + restUnitConverterMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingUnitConverter() throws Exception { + // Initialize the database + insertedUnitConverter = unitConverterRepository.saveAndFlush(unitConverter); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unitConverter + UnitConverter updatedUnitConverter = unitConverterRepository.findById(unitConverter.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedUnitConverter are not directly saved in db + em.detach(updatedUnitConverter); + updatedUnitConverter + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .convertionFormula(UPDATED_CONVERTION_FORMULA) + .version(UPDATED_VERSION); + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(updatedUnitConverter); + + restUnitConverterMockMvc + .perform( + put(ENTITY_API_URL_ID, unitConverterDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isOk()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedUnitConverterToMatchAllProperties(updatedUnitConverter); + } + + @Test + @Transactional + void putNonExistingUnitConverter() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitConverter.setId(longCount.incrementAndGet()); + + // Create the UnitConverter + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restUnitConverterMockMvc + .perform( + put(ENTITY_API_URL_ID, unitConverterDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchUnitConverter() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitConverter.setId(longCount.incrementAndGet()); + + // Create the UnitConverter + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitConverterMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamUnitConverter() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitConverter.setId(longCount.incrementAndGet()); + + // Create the UnitConverter + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitConverterMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateUnitConverterWithPatch() throws Exception { + // Initialize the database + insertedUnitConverter = unitConverterRepository.saveAndFlush(unitConverter); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unitConverter using partial update + UnitConverter partialUpdatedUnitConverter = new UnitConverter(); + partialUpdatedUnitConverter.setId(unitConverter.getId()); + + partialUpdatedUnitConverter.name(UPDATED_NAME).description(UPDATED_DESCRIPTION).version(UPDATED_VERSION); + + restUnitConverterMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedUnitConverter.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedUnitConverter)) + ) + .andExpect(status().isOk()); + + // Validate the UnitConverter in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertUnitConverterUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedUnitConverter, unitConverter), + getPersistedUnitConverter(unitConverter) + ); + } + + @Test + @Transactional + void fullUpdateUnitConverterWithPatch() throws Exception { + // Initialize the database + insertedUnitConverter = unitConverterRepository.saveAndFlush(unitConverter); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unitConverter using partial update + UnitConverter partialUpdatedUnitConverter = new UnitConverter(); + partialUpdatedUnitConverter.setId(unitConverter.getId()); + + partialUpdatedUnitConverter + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .convertionFormula(UPDATED_CONVERTION_FORMULA) + .version(UPDATED_VERSION); + + restUnitConverterMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedUnitConverter.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedUnitConverter)) + ) + .andExpect(status().isOk()); + + // Validate the UnitConverter in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertUnitConverterUpdatableFieldsEquals(partialUpdatedUnitConverter, getPersistedUnitConverter(partialUpdatedUnitConverter)); + } + + @Test + @Transactional + void patchNonExistingUnitConverter() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitConverter.setId(longCount.incrementAndGet()); + + // Create the UnitConverter + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restUnitConverterMockMvc + .perform( + patch(ENTITY_API_URL_ID, unitConverterDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchUnitConverter() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitConverter.setId(longCount.incrementAndGet()); + + // Create the UnitConverter + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitConverterMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamUnitConverter() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitConverter.setId(longCount.incrementAndGet()); + + // Create the UnitConverter + UnitConverterDTO unitConverterDTO = unitConverterMapper.toDto(unitConverter); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitConverterMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(unitConverterDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the UnitConverter in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteUnitConverter() throws Exception { + // Initialize the database + insertedUnitConverter = unitConverterRepository.saveAndFlush(unitConverter); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the unitConverter + restUnitConverterMockMvc + .perform(delete(ENTITY_API_URL_ID, unitConverter.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return unitConverterRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected UnitConverter getPersistedUnitConverter(UnitConverter unitConverter) { + return unitConverterRepository.findById(unitConverter.getId()).orElseThrow(); + } + + protected void assertPersistedUnitConverterToMatchAllProperties(UnitConverter expectedUnitConverter) { + assertUnitConverterAllPropertiesEquals(expectedUnitConverter, getPersistedUnitConverter(expectedUnitConverter)); + } + + protected void assertPersistedUnitConverterToMatchUpdatableProperties(UnitConverter expectedUnitConverter) { + assertUnitConverterAllUpdatablePropertiesEquals(expectedUnitConverter, getPersistedUnitConverter(expectedUnitConverter)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/UnitResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/UnitResourceIT.java new file mode 100644 index 0000000..1df9253 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/UnitResourceIT.java @@ -0,0 +1,590 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.UnitAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static com.oguerreiro.resilient.web.rest.TestUtil.sameNumber; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.repository.UnitRepository; +import com.oguerreiro.resilient.service.UnitService; +import com.oguerreiro.resilient.service.dto.UnitDTO; +import com.oguerreiro.resilient.service.mapper.UnitMapper; +import jakarta.persistence.EntityManager; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link UnitResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class UnitResourceIT { + + private static final String DEFAULT_CODE = "AAAAAAAAAA"; + private static final String UPDATED_CODE = "BBBBBBBBBB"; + + private static final String DEFAULT_SYMBOL = "AAAAAAAAAA"; + private static final String UPDATED_SYMBOL = "BBBBBBBBBB"; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final BigDecimal DEFAULT_CONVERTION_RATE = new BigDecimal(1); + private static final BigDecimal UPDATED_CONVERTION_RATE = new BigDecimal(2); + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/units"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private UnitRepository unitRepository; + + @Mock + private UnitRepository unitRepositoryMock; + + @Autowired + private UnitMapper unitMapper; + + @Mock + private UnitService unitServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restUnitMockMvc; + + private Unit unit; + + private Unit insertedUnit; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Unit createEntity(EntityManager em) { + Unit unit = new Unit() + .code(DEFAULT_CODE) + .symbol(DEFAULT_SYMBOL) + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .convertionRate(DEFAULT_CONVERTION_RATE) + .version(DEFAULT_VERSION); + return unit; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Unit createUpdatedEntity(EntityManager em) { + Unit unit = new Unit() + .code(UPDATED_CODE) + .symbol(UPDATED_SYMBOL) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .convertionRate(UPDATED_CONVERTION_RATE) + .version(UPDATED_VERSION); + return unit; + } + + @BeforeEach + public void initTest() { + unit = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedUnit != null) { + unitRepository.delete(insertedUnit); + insertedUnit = null; + } + } + + @Test + @Transactional + void createUnit() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the Unit + UnitDTO unitDTO = unitMapper.toDto(unit); + var returnedUnitDTO = om.readValue( + restUnitMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + UnitDTO.class + ); + + // Validate the Unit in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedUnit = unitMapper.toEntity(returnedUnitDTO); + assertUnitUpdatableFieldsEquals(returnedUnit, getPersistedUnit(returnedUnit)); + + insertedUnit = returnedUnit; + } + + @Test + @Transactional + void createUnitWithExistingId() throws Exception { + // Create the Unit with an existing ID + unit.setId(1L); + UnitDTO unitDTO = unitMapper.toDto(unit); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restUnitMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isBadRequest()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unit.setCode(null); + + // Create the Unit, which fails. + UnitDTO unitDTO = unitMapper.toDto(unit); + + restUnitMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkSymbolIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unit.setSymbol(null); + + // Create the Unit, which fails. + UnitDTO unitDTO = unitMapper.toDto(unit); + + restUnitMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unit.setName(null); + + // Create the Unit, which fails. + UnitDTO unitDTO = unitMapper.toDto(unit); + + restUnitMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkConvertionRateIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unit.setConvertionRate(null); + + // Create the Unit, which fails. + UnitDTO unitDTO = unitMapper.toDto(unit); + + restUnitMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllUnits() throws Exception { + // Initialize the database + insertedUnit = unitRepository.saveAndFlush(unit); + + // Get all the unitList + restUnitMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(unit.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].symbol").value(hasItem(DEFAULT_SYMBOL))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].convertionRate").value(hasItem(sameNumber(DEFAULT_CONVERTION_RATE)))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllUnitsWithEagerRelationshipsIsEnabled() throws Exception { + when(unitServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restUnitMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(unitServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllUnitsWithEagerRelationshipsIsNotEnabled() throws Exception { + when(unitServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restUnitMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(unitRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getUnit() throws Exception { + // Initialize the database + insertedUnit = unitRepository.saveAndFlush(unit); + + // Get the unit + restUnitMockMvc + .perform(get(ENTITY_API_URL_ID, unit.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(unit.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.symbol").value(DEFAULT_SYMBOL)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.convertionRate").value(sameNumber(DEFAULT_CONVERTION_RATE))) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingUnit() throws Exception { + // Get the unit + restUnitMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingUnit() throws Exception { + // Initialize the database + insertedUnit = unitRepository.saveAndFlush(unit); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unit + Unit updatedUnit = unitRepository.findById(unit.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedUnit are not directly saved in db + em.detach(updatedUnit); + updatedUnit + .code(UPDATED_CODE) + .symbol(UPDATED_SYMBOL) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .convertionRate(UPDATED_CONVERTION_RATE) + .version(UPDATED_VERSION); + UnitDTO unitDTO = unitMapper.toDto(updatedUnit); + + restUnitMockMvc + .perform( + put(ENTITY_API_URL_ID, unitDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitDTO)) + ) + .andExpect(status().isOk()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedUnitToMatchAllProperties(updatedUnit); + } + + @Test + @Transactional + void putNonExistingUnit() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unit.setId(longCount.incrementAndGet()); + + // Create the Unit + UnitDTO unitDTO = unitMapper.toDto(unit); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restUnitMockMvc + .perform( + put(ENTITY_API_URL_ID, unitDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchUnit() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unit.setId(longCount.incrementAndGet()); + + // Create the Unit + UnitDTO unitDTO = unitMapper.toDto(unit); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamUnit() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unit.setId(longCount.incrementAndGet()); + + // Create the Unit + UnitDTO unitDTO = unitMapper.toDto(unit); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitMockMvc + .perform(put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateUnitWithPatch() throws Exception { + // Initialize the database + insertedUnit = unitRepository.saveAndFlush(unit); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unit using partial update + Unit partialUpdatedUnit = new Unit(); + partialUpdatedUnit.setId(unit.getId()); + + partialUpdatedUnit.code(UPDATED_CODE).name(UPDATED_NAME).description(UPDATED_DESCRIPTION); + + restUnitMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedUnit.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedUnit)) + ) + .andExpect(status().isOk()); + + // Validate the Unit in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertUnitUpdatableFieldsEquals(createUpdateProxyForBean(partialUpdatedUnit, unit), getPersistedUnit(unit)); + } + + @Test + @Transactional + void fullUpdateUnitWithPatch() throws Exception { + // Initialize the database + insertedUnit = unitRepository.saveAndFlush(unit); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unit using partial update + Unit partialUpdatedUnit = new Unit(); + partialUpdatedUnit.setId(unit.getId()); + + partialUpdatedUnit + .code(UPDATED_CODE) + .symbol(UPDATED_SYMBOL) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .convertionRate(UPDATED_CONVERTION_RATE) + .version(UPDATED_VERSION); + + restUnitMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedUnit.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedUnit)) + ) + .andExpect(status().isOk()); + + // Validate the Unit in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertUnitUpdatableFieldsEquals(partialUpdatedUnit, getPersistedUnit(partialUpdatedUnit)); + } + + @Test + @Transactional + void patchNonExistingUnit() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unit.setId(longCount.incrementAndGet()); + + // Create the Unit + UnitDTO unitDTO = unitMapper.toDto(unit); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restUnitMockMvc + .perform( + patch(ENTITY_API_URL_ID, unitDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(unitDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchUnit() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unit.setId(longCount.incrementAndGet()); + + // Create the Unit + UnitDTO unitDTO = unitMapper.toDto(unit); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(unitDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamUnit() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unit.setId(longCount.incrementAndGet()); + + // Create the Unit + UnitDTO unitDTO = unitMapper.toDto(unit); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitMockMvc + .perform(patch(ENTITY_API_URL).with(csrf()).contentType("application/merge-patch+json").content(om.writeValueAsBytes(unitDTO))) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Unit in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteUnit() throws Exception { + // Initialize the database + insertedUnit = unitRepository.saveAndFlush(unit); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the unit + restUnitMockMvc + .perform(delete(ENTITY_API_URL_ID, unit.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return unitRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected Unit getPersistedUnit(Unit unit) { + return unitRepository.findById(unit.getId()).orElseThrow(); + } + + protected void assertPersistedUnitToMatchAllProperties(Unit expectedUnit) { + assertUnitAllPropertiesEquals(expectedUnit, getPersistedUnit(expectedUnit)); + } + + protected void assertPersistedUnitToMatchUpdatableProperties(Unit expectedUnit) { + assertUnitAllUpdatablePropertiesEquals(expectedUnit, getPersistedUnit(expectedUnit)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/UnitTypeResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/UnitTypeResourceIT.java new file mode 100644 index 0000000..032fad1 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/UnitTypeResourceIT.java @@ -0,0 +1,518 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.UnitTypeAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.UnitType; +import com.oguerreiro.resilient.domain.enumeration.UnitValueType; +import com.oguerreiro.resilient.repository.UnitTypeRepository; +import com.oguerreiro.resilient.service.dto.UnitTypeDTO; +import com.oguerreiro.resilient.service.mapper.UnitTypeMapper; +import jakarta.persistence.EntityManager; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link UnitTypeResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +@WithMockUser +class UnitTypeResourceIT { + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final UnitValueType DEFAULT_VALUE_TYPE = UnitValueType.DECIMAL; + private static final UnitValueType UPDATED_VALUE_TYPE = UnitValueType.BOOLEAN; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/unit-types"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private UnitTypeRepository unitTypeRepository; + + @Autowired + private UnitTypeMapper unitTypeMapper; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restUnitTypeMockMvc; + + private UnitType unitType; + + private UnitType insertedUnitType; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static UnitType createEntity(EntityManager em) { + UnitType unitType = new UnitType() + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .valueType(DEFAULT_VALUE_TYPE) + .version(DEFAULT_VERSION); + return unitType; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static UnitType createUpdatedEntity(EntityManager em) { + UnitType unitType = new UnitType() + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .valueType(UPDATED_VALUE_TYPE) + .version(UPDATED_VERSION); + return unitType; + } + + @BeforeEach + public void initTest() { + unitType = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedUnitType != null) { + unitTypeRepository.delete(insertedUnitType); + insertedUnitType = null; + } + } + + @Test + @Transactional + void createUnitType() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the UnitType + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + var returnedUnitTypeDTO = om.readValue( + restUnitTypeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitTypeDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + UnitTypeDTO.class + ); + + // Validate the UnitType in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedUnitType = unitTypeMapper.toEntity(returnedUnitTypeDTO); + assertUnitTypeUpdatableFieldsEquals(returnedUnitType, getPersistedUnitType(returnedUnitType)); + + insertedUnitType = returnedUnitType; + } + + @Test + @Transactional + void createUnitTypeWithExistingId() throws Exception { + // Create the UnitType with an existing ID + unitType.setId(1L); + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restUnitTypeMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitTypeDTO))) + .andExpect(status().isBadRequest()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unitType.setName(null); + + // Create the UnitType, which fails. + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + restUnitTypeMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitTypeDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkDescriptionIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unitType.setDescription(null); + + // Create the UnitType, which fails. + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + restUnitTypeMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitTypeDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkValueTypeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + unitType.setValueType(null); + + // Create the UnitType, which fails. + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + restUnitTypeMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitTypeDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllUnitTypes() throws Exception { + // Initialize the database + insertedUnitType = unitTypeRepository.saveAndFlush(unitType); + + // Get all the unitTypeList + restUnitTypeMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(unitType.getId().intValue()))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].valueType").value(hasItem(DEFAULT_VALUE_TYPE.toString()))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @Test + @Transactional + void getUnitType() throws Exception { + // Initialize the database + insertedUnitType = unitTypeRepository.saveAndFlush(unitType); + + // Get the unitType + restUnitTypeMockMvc + .perform(get(ENTITY_API_URL_ID, unitType.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(unitType.getId().intValue())) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.valueType").value(DEFAULT_VALUE_TYPE.toString())) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingUnitType() throws Exception { + // Get the unitType + restUnitTypeMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingUnitType() throws Exception { + // Initialize the database + insertedUnitType = unitTypeRepository.saveAndFlush(unitType); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unitType + UnitType updatedUnitType = unitTypeRepository.findById(unitType.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedUnitType are not directly saved in db + em.detach(updatedUnitType); + updatedUnitType.name(UPDATED_NAME).description(UPDATED_DESCRIPTION).valueType(UPDATED_VALUE_TYPE).version(UPDATED_VERSION); + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(updatedUnitType); + + restUnitTypeMockMvc + .perform( + put(ENTITY_API_URL_ID, unitTypeDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitTypeDTO)) + ) + .andExpect(status().isOk()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedUnitTypeToMatchAllProperties(updatedUnitType); + } + + @Test + @Transactional + void putNonExistingUnitType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitType.setId(longCount.incrementAndGet()); + + // Create the UnitType + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restUnitTypeMockMvc + .perform( + put(ENTITY_API_URL_ID, unitTypeDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchUnitType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitType.setId(longCount.incrementAndGet()); + + // Create the UnitType + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitTypeMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(unitTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamUnitType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitType.setId(longCount.incrementAndGet()); + + // Create the UnitType + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitTypeMockMvc + .perform(put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(unitTypeDTO))) + .andExpect(status().isMethodNotAllowed()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateUnitTypeWithPatch() throws Exception { + // Initialize the database + insertedUnitType = unitTypeRepository.saveAndFlush(unitType); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unitType using partial update + UnitType partialUpdatedUnitType = new UnitType(); + partialUpdatedUnitType.setId(unitType.getId()); + + partialUpdatedUnitType.version(UPDATED_VERSION); + + restUnitTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedUnitType.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedUnitType)) + ) + .andExpect(status().isOk()); + + // Validate the UnitType in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertUnitTypeUpdatableFieldsEquals(createUpdateProxyForBean(partialUpdatedUnitType, unitType), getPersistedUnitType(unitType)); + } + + @Test + @Transactional + void fullUpdateUnitTypeWithPatch() throws Exception { + // Initialize the database + insertedUnitType = unitTypeRepository.saveAndFlush(unitType); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the unitType using partial update + UnitType partialUpdatedUnitType = new UnitType(); + partialUpdatedUnitType.setId(unitType.getId()); + + partialUpdatedUnitType.name(UPDATED_NAME).description(UPDATED_DESCRIPTION).valueType(UPDATED_VALUE_TYPE).version(UPDATED_VERSION); + + restUnitTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedUnitType.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedUnitType)) + ) + .andExpect(status().isOk()); + + // Validate the UnitType in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertUnitTypeUpdatableFieldsEquals(partialUpdatedUnitType, getPersistedUnitType(partialUpdatedUnitType)); + } + + @Test + @Transactional + void patchNonExistingUnitType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitType.setId(longCount.incrementAndGet()); + + // Create the UnitType + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restUnitTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, unitTypeDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(unitTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchUnitType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitType.setId(longCount.incrementAndGet()); + + // Create the UnitType + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitTypeMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(unitTypeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamUnitType() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + unitType.setId(longCount.incrementAndGet()); + + // Create the UnitType + UnitTypeDTO unitTypeDTO = unitTypeMapper.toDto(unitType); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restUnitTypeMockMvc + .perform( + patch(ENTITY_API_URL).with(csrf()).contentType("application/merge-patch+json").content(om.writeValueAsBytes(unitTypeDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the UnitType in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteUnitType() throws Exception { + // Initialize the database + insertedUnitType = unitTypeRepository.saveAndFlush(unitType); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the unitType + restUnitTypeMockMvc + .perform(delete(ENTITY_API_URL_ID, unitType.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return unitTypeRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected UnitType getPersistedUnitType(UnitType unitType) { + return unitTypeRepository.findById(unitType.getId()).orElseThrow(); + } + + protected void assertPersistedUnitTypeToMatchAllProperties(UnitType expectedUnitType) { + assertUnitTypeAllPropertiesEquals(expectedUnitType, getPersistedUnitType(expectedUnitType)); + } + + protected void assertPersistedUnitTypeToMatchUpdatableProperties(UnitType expectedUnitType) { + assertUnitTypeAllUpdatablePropertiesEquals(expectedUnitType, getPersistedUnitType(expectedUnitType)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/UserResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/UserResourceIT.java new file mode 100644 index 0000000..d25d472 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/UserResourceIT.java @@ -0,0 +1,504 @@ +package com.oguerreiro.resilient.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.User; +import com.oguerreiro.resilient.repository.UserRepository; +import com.oguerreiro.resilient.security.AuthoritiesConstants; +import com.oguerreiro.resilient.service.UserService; +import com.oguerreiro.resilient.service.dto.AdminUserDTO; +import com.oguerreiro.resilient.service.mapper.UserMapper; +import jakarta.persistence.EntityManager; +import java.util.*; +import java.util.Objects; +import java.util.function.Consumer; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link UserResource} REST controller. + */ +@AutoConfigureMockMvc +@WithMockUser(authorities = AuthoritiesConstants.ADMIN) +@IntegrationTest +class UserResourceIT { + + private static final String DEFAULT_LOGIN = "johndoe"; + private static final String UPDATED_LOGIN = "jhipster"; + + private static final Long DEFAULT_ID = 1L; + + private static final String DEFAULT_PASSWORD = "passjohndoe"; + private static final String UPDATED_PASSWORD = "passjhipster"; + + private static final String DEFAULT_EMAIL = "johndoe@localhost"; + private static final String UPDATED_EMAIL = "jhipster@localhost"; + + private static final String DEFAULT_FIRSTNAME = "john"; + private static final String UPDATED_FIRSTNAME = "jhipsterFirstName"; + + private static final String DEFAULT_LASTNAME = "doe"; + private static final String UPDATED_LASTNAME = "jhipsterLastName"; + + private static final String DEFAULT_IMAGEURL = "http://placehold.it/50x50"; + private static final String UPDATED_IMAGEURL = "http://placehold.it/40x40"; + + private static final String DEFAULT_LANGKEY = "en"; + private static final String UPDATED_LANGKEY = "fr"; + + @Autowired + private ObjectMapper om; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + private UserMapper userMapper; + + @Autowired + private EntityManager em; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private MockMvc restUserMockMvc; + + private User user; + + private Long numberOfUsers; + + @BeforeEach + public void countUsers() { + numberOfUsers = userRepository.count(); + } + + /** + * Create a User. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which has a required relationship to the User entity. + */ + public static User createEntity(EntityManager em) { + User persistUser = new User(); + persistUser.setLogin(DEFAULT_LOGIN + RandomStringUtils.randomAlphabetic(5)); + persistUser.setPassword(RandomStringUtils.randomAlphanumeric(60)); + persistUser.setActivated(true); + persistUser.setEmail(RandomStringUtils.randomAlphabetic(5) + DEFAULT_EMAIL); + persistUser.setFirstName(DEFAULT_FIRSTNAME); + persistUser.setLastName(DEFAULT_LASTNAME); + persistUser.setImageUrl(DEFAULT_IMAGEURL); + persistUser.setLangKey(DEFAULT_LANGKEY); + return persistUser; + } + + /** + * Setups the database with one user. + */ + public static User initTestUser(EntityManager em) { + User persistUser = createEntity(em); + persistUser.setLogin(DEFAULT_LOGIN); + persistUser.setEmail(DEFAULT_EMAIL); + return persistUser; + } + + @BeforeEach + public void initTest() { + user = initTestUser(em); + } + + @AfterEach + public void cleanupAndCheck() { + cacheManager + .getCacheNames() + .stream() + .map(cacheName -> this.cacheManager.getCache(cacheName)) + .filter(Objects::nonNull) + .forEach(Cache::clear); + userService.deleteUser(DEFAULT_LOGIN); + userService.deleteUser(UPDATED_LOGIN); + userService.deleteUser(user.getLogin()); + userService.deleteUser("anotherlogin"); + assertThat(userRepository.count()).isEqualTo(numberOfUsers); + numberOfUsers = null; + } + + @Test + @Transactional + void createUser() throws Exception { + // Create the User + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setLogin(DEFAULT_LOGIN); + userDTO.setFirstName(DEFAULT_FIRSTNAME); + userDTO.setLastName(DEFAULT_LASTNAME); + userDTO.setEmail(DEFAULT_EMAIL); + userDTO.setActivated(true); + userDTO.setImageUrl(DEFAULT_IMAGEURL); + userDTO.setLangKey(DEFAULT_LANGKEY); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + var returnedUserDTO = om.readValue( + restUserMockMvc + .perform( + post("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf()) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + AdminUserDTO.class + ); + + User convertedUser = userMapper.userDTOToUser(returnedUserDTO); + // Validate the returned User + assertThat(convertedUser.getLogin()).isEqualTo(DEFAULT_LOGIN); + assertThat(convertedUser.getFirstName()).isEqualTo(DEFAULT_FIRSTNAME); + assertThat(convertedUser.getLastName()).isEqualTo(DEFAULT_LASTNAME); + assertThat(convertedUser.getEmail()).isEqualTo(DEFAULT_EMAIL); + assertThat(convertedUser.getImageUrl()).isEqualTo(DEFAULT_IMAGEURL); + assertThat(convertedUser.getLangKey()).isEqualTo(DEFAULT_LANGKEY); + } + + @Test + @Transactional + void createUserWithExistingId() throws Exception { + int databaseSizeBeforeCreate = userRepository.findAll().size(); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setId(DEFAULT_ID); + userDTO.setLogin(DEFAULT_LOGIN); + userDTO.setFirstName(DEFAULT_FIRSTNAME); + userDTO.setLastName(DEFAULT_LASTNAME); + userDTO.setEmail(DEFAULT_EMAIL); + userDTO.setActivated(true); + userDTO.setImageUrl(DEFAULT_IMAGEURL); + userDTO.setLangKey(DEFAULT_LANGKEY); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + // An entity with an existing ID cannot be created, so this API call must fail + restUserMockMvc + .perform(post("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + // Validate the User in the database + assertPersistedUsers(users -> assertThat(users).hasSize(databaseSizeBeforeCreate)); + } + + @Test + @Transactional + void createUserWithExistingLogin() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeCreate = userRepository.findAll().size(); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setLogin(DEFAULT_LOGIN); // this login should already be used + userDTO.setFirstName(DEFAULT_FIRSTNAME); + userDTO.setLastName(DEFAULT_LASTNAME); + userDTO.setEmail("anothermail@localhost"); + userDTO.setActivated(true); + userDTO.setImageUrl(DEFAULT_IMAGEURL); + userDTO.setLangKey(DEFAULT_LANGKEY); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + // Create the User + restUserMockMvc + .perform(post("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + // Validate the User in the database + assertPersistedUsers(users -> assertThat(users).hasSize(databaseSizeBeforeCreate)); + } + + @Test + @Transactional + void createUserWithExistingEmail() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeCreate = userRepository.findAll().size(); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setLogin("anotherlogin"); + userDTO.setFirstName(DEFAULT_FIRSTNAME); + userDTO.setLastName(DEFAULT_LASTNAME); + userDTO.setEmail(DEFAULT_EMAIL); // this email should already be used + userDTO.setActivated(true); + userDTO.setImageUrl(DEFAULT_IMAGEURL); + userDTO.setLangKey(DEFAULT_LANGKEY); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + // Create the User + restUserMockMvc + .perform(post("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + // Validate the User in the database + assertPersistedUsers(users -> assertThat(users).hasSize(databaseSizeBeforeCreate)); + } + + @Test + @Transactional + void getAllUsers() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + // Get all the users + restUserMockMvc + .perform(get("/api/admin/users?sort=id,desc").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].login").value(hasItem(DEFAULT_LOGIN))) + .andExpect(jsonPath("$.[*].firstName").value(hasItem(DEFAULT_FIRSTNAME))) + .andExpect(jsonPath("$.[*].lastName").value(hasItem(DEFAULT_LASTNAME))) + .andExpect(jsonPath("$.[*].email").value(hasItem(DEFAULT_EMAIL))) + .andExpect(jsonPath("$.[*].imageUrl").value(hasItem(DEFAULT_IMAGEURL))) + .andExpect(jsonPath("$.[*].langKey").value(hasItem(DEFAULT_LANGKEY))); + } + + @Test + @Transactional + void getUser() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + assertThat(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE).get(user.getLogin())).isNull(); + + // Get the user + restUserMockMvc + .perform(get("/api/admin/users/{login}", user.getLogin())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.login").value(user.getLogin())) + .andExpect(jsonPath("$.firstName").value(DEFAULT_FIRSTNAME)) + .andExpect(jsonPath("$.lastName").value(DEFAULT_LASTNAME)) + .andExpect(jsonPath("$.email").value(DEFAULT_EMAIL)) + .andExpect(jsonPath("$.imageUrl").value(DEFAULT_IMAGEURL)) + .andExpect(jsonPath("$.langKey").value(DEFAULT_LANGKEY)); + + assertThat(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE).get(user.getLogin())).isNotNull(); + } + + @Test + @Transactional + void getNonExistingUser() throws Exception { + restUserMockMvc.perform(get("/api/admin/users/unknown")).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void updateUser() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeUpdate = userRepository.findAll().size(); + + // Update the user + User updatedUser = userRepository.findById(user.getId()).orElseThrow(); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setId(updatedUser.getId()); + userDTO.setLogin(updatedUser.getLogin()); + userDTO.setFirstName(UPDATED_FIRSTNAME); + userDTO.setLastName(UPDATED_LASTNAME); + userDTO.setEmail(UPDATED_EMAIL); + userDTO.setActivated(updatedUser.isActivated()); + userDTO.setImageUrl(UPDATED_IMAGEURL); + userDTO.setLangKey(UPDATED_LANGKEY); + userDTO.setCreatedBy(updatedUser.getCreatedBy()); + userDTO.setCreatedDate(updatedUser.getCreatedDate()); + userDTO.setLastModifiedBy(updatedUser.getLastModifiedBy()); + userDTO.setLastModifiedDate(updatedUser.getLastModifiedDate()); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + restUserMockMvc + .perform(put("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isOk()); + + // Validate the User in the database + assertPersistedUsers(users -> { + assertThat(users).hasSize(databaseSizeBeforeUpdate); + User testUser = users.stream().filter(usr -> usr.getId().equals(updatedUser.getId())).findFirst().orElseThrow(); + assertThat(testUser.getFirstName()).isEqualTo(UPDATED_FIRSTNAME); + assertThat(testUser.getLastName()).isEqualTo(UPDATED_LASTNAME); + assertThat(testUser.getEmail()).isEqualTo(UPDATED_EMAIL); + assertThat(testUser.getImageUrl()).isEqualTo(UPDATED_IMAGEURL); + assertThat(testUser.getLangKey()).isEqualTo(UPDATED_LANGKEY); + }); + } + + @Test + @Transactional + void updateUserLogin() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeUpdate = userRepository.findAll().size(); + + // Update the user + User updatedUser = userRepository.findById(user.getId()).orElseThrow(); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setId(updatedUser.getId()); + userDTO.setLogin(UPDATED_LOGIN); + userDTO.setFirstName(UPDATED_FIRSTNAME); + userDTO.setLastName(UPDATED_LASTNAME); + userDTO.setEmail(UPDATED_EMAIL); + userDTO.setActivated(updatedUser.isActivated()); + userDTO.setImageUrl(UPDATED_IMAGEURL); + userDTO.setLangKey(UPDATED_LANGKEY); + userDTO.setCreatedBy(updatedUser.getCreatedBy()); + userDTO.setCreatedDate(updatedUser.getCreatedDate()); + userDTO.setLastModifiedBy(updatedUser.getLastModifiedBy()); + userDTO.setLastModifiedDate(updatedUser.getLastModifiedDate()); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + restUserMockMvc + .perform(put("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isOk()); + + // Validate the User in the database + assertPersistedUsers(users -> { + assertThat(users).hasSize(databaseSizeBeforeUpdate); + User testUser = users.stream().filter(usr -> usr.getId().equals(updatedUser.getId())).findFirst().orElseThrow(); + assertThat(testUser.getLogin()).isEqualTo(UPDATED_LOGIN); + assertThat(testUser.getFirstName()).isEqualTo(UPDATED_FIRSTNAME); + assertThat(testUser.getLastName()).isEqualTo(UPDATED_LASTNAME); + assertThat(testUser.getEmail()).isEqualTo(UPDATED_EMAIL); + assertThat(testUser.getImageUrl()).isEqualTo(UPDATED_IMAGEURL); + assertThat(testUser.getLangKey()).isEqualTo(UPDATED_LANGKEY); + }); + } + + @Test + @Transactional + void updateUserExistingEmail() throws Exception { + // Initialize the database with 2 users + userRepository.saveAndFlush(user); + + User anotherUser = new User(); + anotherUser.setLogin("jhipster"); + anotherUser.setPassword(RandomStringUtils.randomAlphanumeric(60)); + anotherUser.setActivated(true); + anotherUser.setEmail("jhipster@localhost"); + anotherUser.setFirstName("java"); + anotherUser.setLastName("hipster"); + anotherUser.setImageUrl(""); + anotherUser.setLangKey("en"); + userRepository.saveAndFlush(anotherUser); + + // Update the user + User updatedUser = userRepository.findById(user.getId()).orElseThrow(); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setId(updatedUser.getId()); + userDTO.setLogin(updatedUser.getLogin()); + userDTO.setFirstName(updatedUser.getFirstName()); + userDTO.setLastName(updatedUser.getLastName()); + userDTO.setEmail("jhipster@localhost"); // this email should already be used by anotherUser + userDTO.setActivated(updatedUser.isActivated()); + userDTO.setImageUrl(updatedUser.getImageUrl()); + userDTO.setLangKey(updatedUser.getLangKey()); + userDTO.setCreatedBy(updatedUser.getCreatedBy()); + userDTO.setCreatedDate(updatedUser.getCreatedDate()); + userDTO.setLastModifiedBy(updatedUser.getLastModifiedBy()); + userDTO.setLastModifiedDate(updatedUser.getLastModifiedDate()); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + restUserMockMvc + .perform(put("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + } + + @Test + @Transactional + void updateUserExistingLogin() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + + User anotherUser = new User(); + anotherUser.setLogin("jhipster"); + anotherUser.setPassword(RandomStringUtils.randomAlphanumeric(60)); + anotherUser.setActivated(true); + anotherUser.setEmail("jhipster@localhost"); + anotherUser.setFirstName("java"); + anotherUser.setLastName("hipster"); + anotherUser.setImageUrl(""); + anotherUser.setLangKey("en"); + userRepository.saveAndFlush(anotherUser); + + // Update the user + User updatedUser = userRepository.findById(user.getId()).orElseThrow(); + + AdminUserDTO userDTO = new AdminUserDTO(); + userDTO.setId(updatedUser.getId()); + userDTO.setLogin("jhipster"); // this login should already be used by anotherUser + userDTO.setFirstName(updatedUser.getFirstName()); + userDTO.setLastName(updatedUser.getLastName()); + userDTO.setEmail(updatedUser.getEmail()); + userDTO.setActivated(updatedUser.isActivated()); + userDTO.setImageUrl(updatedUser.getImageUrl()); + userDTO.setLangKey(updatedUser.getLangKey()); + userDTO.setCreatedBy(updatedUser.getCreatedBy()); + userDTO.setCreatedDate(updatedUser.getCreatedDate()); + userDTO.setLastModifiedBy(updatedUser.getLastModifiedBy()); + userDTO.setLastModifiedDate(updatedUser.getLastModifiedDate()); + userDTO.setAuthorities(Collections.singleton(AuthoritiesConstants.USER)); + + restUserMockMvc + .perform(put("/api/admin/users").contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(userDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + } + + @Test + @Transactional + void deleteUser() throws Exception { + // Initialize the database + userRepository.saveAndFlush(user); + int databaseSizeBeforeDelete = userRepository.findAll().size(); + + // Delete the user + restUserMockMvc + .perform(delete("/api/admin/users/{login}", user.getLogin()).accept(MediaType.APPLICATION_JSON).with(csrf())) + .andExpect(status().isNoContent()); + + assertThat(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE).get(user.getLogin())).isNull(); + + // Validate the database is empty + assertPersistedUsers(users -> assertThat(users).hasSize(databaseSizeBeforeDelete - 1)); + } + + @Test + void testUserEquals() throws Exception { + TestUtil.equalsVerifier(User.class); + User user1 = new User(); + user1.setId(DEFAULT_ID); + User user2 = new User(); + user2.setId(user1.getId()); + assertThat(user1).isEqualTo(user2); + user2.setId(2L); + assertThat(user1).isNotEqualTo(user2); + user1.setId(null); + assertThat(user1).isNotEqualTo(user2); + } + + private void assertPersistedUsers(Consumer> userAssertion) { + userAssertion.accept(userRepository.findAll()); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/VariableCategoryResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/VariableCategoryResourceIT.java new file mode 100644 index 0000000..4ad7ad6 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/VariableCategoryResourceIT.java @@ -0,0 +1,572 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.VariableCategoryAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.repository.VariableCategoryRepository; +import com.oguerreiro.resilient.service.VariableCategoryService; +import com.oguerreiro.resilient.service.dto.VariableCategoryDTO; +import com.oguerreiro.resilient.service.mapper.VariableCategoryMapper; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link VariableCategoryResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class VariableCategoryResourceIT { + + private static final String DEFAULT_CODE = "30540"; + private static final String UPDATED_CODE = "0330"; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/variable-categories"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private VariableCategoryRepository variableCategoryRepository; + + @Mock + private VariableCategoryRepository variableCategoryRepositoryMock; + + @Autowired + private VariableCategoryMapper variableCategoryMapper; + + @Mock + private VariableCategoryService variableCategoryServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restVariableCategoryMockMvc; + + private VariableCategory variableCategory; + + private VariableCategory insertedVariableCategory; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableCategory createEntity(EntityManager em) { + VariableCategory variableCategory = new VariableCategory() + .code(DEFAULT_CODE) + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .version(DEFAULT_VERSION); + return variableCategory; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableCategory createUpdatedEntity(EntityManager em) { + VariableCategory variableCategory = new VariableCategory() + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .version(UPDATED_VERSION); + return variableCategory; + } + + @BeforeEach + public void initTest() { + variableCategory = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedVariableCategory != null) { + variableCategoryRepository.delete(insertedVariableCategory); + insertedVariableCategory = null; + } + } + + @Test + @Transactional + void createVariableCategory() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the VariableCategory + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + var returnedVariableCategoryDTO = om.readValue( + restVariableCategoryMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + VariableCategoryDTO.class + ); + + // Validate the VariableCategory in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedVariableCategory = variableCategoryMapper.toEntity(returnedVariableCategoryDTO); + assertVariableCategoryUpdatableFieldsEquals(returnedVariableCategory, getPersistedVariableCategory(returnedVariableCategory)); + + insertedVariableCategory = returnedVariableCategory; + } + + @Test + @Transactional + void createVariableCategoryWithExistingId() throws Exception { + // Create the VariableCategory with an existing ID + variableCategory.setId(1L); + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restVariableCategoryMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableCategory.setCode(null); + + // Create the VariableCategory, which fails. + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + restVariableCategoryMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableCategory.setName(null); + + // Create the VariableCategory, which fails. + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + restVariableCategoryMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkDescriptionIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableCategory.setDescription(null); + + // Create the VariableCategory, which fails. + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + restVariableCategoryMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllVariableCategories() throws Exception { + // Initialize the database + insertedVariableCategory = variableCategoryRepository.saveAndFlush(variableCategory); + + // Get all the variableCategoryList + restVariableCategoryMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(variableCategory.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariableCategoriesWithEagerRelationshipsIsEnabled() throws Exception { + when(variableCategoryServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableCategoryMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(variableCategoryServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariableCategoriesWithEagerRelationshipsIsNotEnabled() throws Exception { + when(variableCategoryServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableCategoryMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(variableCategoryRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getVariableCategory() throws Exception { + // Initialize the database + insertedVariableCategory = variableCategoryRepository.saveAndFlush(variableCategory); + + // Get the variableCategory + restVariableCategoryMockMvc + .perform(get(ENTITY_API_URL_ID, variableCategory.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(variableCategory.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingVariableCategory() throws Exception { + // Get the variableCategory + restVariableCategoryMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingVariableCategory() throws Exception { + // Initialize the database + insertedVariableCategory = variableCategoryRepository.saveAndFlush(variableCategory); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableCategory + VariableCategory updatedVariableCategory = variableCategoryRepository.findById(variableCategory.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedVariableCategory are not directly saved in db + em.detach(updatedVariableCategory); + updatedVariableCategory.code(UPDATED_CODE).name(UPDATED_NAME).description(UPDATED_DESCRIPTION).version(UPDATED_VERSION); + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(updatedVariableCategory); + + restVariableCategoryMockMvc + .perform( + put(ENTITY_API_URL_ID, variableCategoryDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isOk()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedVariableCategoryToMatchAllProperties(updatedVariableCategory); + } + + @Test + @Transactional + void putNonExistingVariableCategory() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableCategory.setId(longCount.incrementAndGet()); + + // Create the VariableCategory + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableCategoryMockMvc + .perform( + put(ENTITY_API_URL_ID, variableCategoryDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchVariableCategory() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableCategory.setId(longCount.incrementAndGet()); + + // Create the VariableCategory + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableCategoryMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamVariableCategory() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableCategory.setId(longCount.incrementAndGet()); + + // Create the VariableCategory + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableCategoryMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateVariableCategoryWithPatch() throws Exception { + // Initialize the database + insertedVariableCategory = variableCategoryRepository.saveAndFlush(variableCategory); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableCategory using partial update + VariableCategory partialUpdatedVariableCategory = new VariableCategory(); + partialUpdatedVariableCategory.setId(variableCategory.getId()); + + restVariableCategoryMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableCategory.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableCategory)) + ) + .andExpect(status().isOk()); + + // Validate the VariableCategory in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableCategoryUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedVariableCategory, variableCategory), + getPersistedVariableCategory(variableCategory) + ); + } + + @Test + @Transactional + void fullUpdateVariableCategoryWithPatch() throws Exception { + // Initialize the database + insertedVariableCategory = variableCategoryRepository.saveAndFlush(variableCategory); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableCategory using partial update + VariableCategory partialUpdatedVariableCategory = new VariableCategory(); + partialUpdatedVariableCategory.setId(variableCategory.getId()); + + partialUpdatedVariableCategory.code(UPDATED_CODE).name(UPDATED_NAME).description(UPDATED_DESCRIPTION).version(UPDATED_VERSION); + + restVariableCategoryMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableCategory.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableCategory)) + ) + .andExpect(status().isOk()); + + // Validate the VariableCategory in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableCategoryUpdatableFieldsEquals( + partialUpdatedVariableCategory, + getPersistedVariableCategory(partialUpdatedVariableCategory) + ); + } + + @Test + @Transactional + void patchNonExistingVariableCategory() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableCategory.setId(longCount.incrementAndGet()); + + // Create the VariableCategory + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableCategoryMockMvc + .perform( + patch(ENTITY_API_URL_ID, variableCategoryDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchVariableCategory() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableCategory.setId(longCount.incrementAndGet()); + + // Create the VariableCategory + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableCategoryMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamVariableCategory() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableCategory.setId(longCount.incrementAndGet()); + + // Create the VariableCategory + VariableCategoryDTO variableCategoryDTO = variableCategoryMapper.toDto(variableCategory); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableCategoryMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableCategoryDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableCategory in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteVariableCategory() throws Exception { + // Initialize the database + insertedVariableCategory = variableCategoryRepository.saveAndFlush(variableCategory); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the variableCategory + restVariableCategoryMockMvc + .perform(delete(ENTITY_API_URL_ID, variableCategory.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return variableCategoryRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected VariableCategory getPersistedVariableCategory(VariableCategory variableCategory) { + return variableCategoryRepository.findById(variableCategory.getId()).orElseThrow(); + } + + protected void assertPersistedVariableCategoryToMatchAllProperties(VariableCategory expectedVariableCategory) { + assertVariableCategoryAllPropertiesEquals(expectedVariableCategory, getPersistedVariableCategory(expectedVariableCategory)); + } + + protected void assertPersistedVariableCategoryToMatchUpdatableProperties(VariableCategory expectedVariableCategory) { + assertVariableCategoryAllUpdatablePropertiesEquals( + expectedVariableCategory, + getPersistedVariableCategory(expectedVariableCategory) + ); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/VariableClassResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/VariableClassResourceIT.java new file mode 100644 index 0000000..875dd1e --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/VariableClassResourceIT.java @@ -0,0 +1,536 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.VariableClassAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.VariableClass; +import com.oguerreiro.resilient.repository.VariableClassRepository; +import com.oguerreiro.resilient.service.VariableClassService; +import com.oguerreiro.resilient.service.dto.VariableClassDTO; +import com.oguerreiro.resilient.service.mapper.VariableClassMapper; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link VariableClassResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class VariableClassResourceIT { + + private static final String DEFAULT_CODE = "AAAAAAAAAA"; + private static final String UPDATED_CODE = "BBBBBBBBBB"; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/variable-classes"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private VariableClassRepository variableClassRepository; + + @Mock + private VariableClassRepository variableClassRepositoryMock; + + @Autowired + private VariableClassMapper variableClassMapper; + + @Mock + private VariableClassService variableClassServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restVariableClassMockMvc; + + private VariableClass variableClass; + + private VariableClass insertedVariableClass; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableClass createEntity(EntityManager em) { + VariableClass variableClass = new VariableClass().code(DEFAULT_CODE).name(DEFAULT_NAME).version(DEFAULT_VERSION); + return variableClass; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableClass createUpdatedEntity(EntityManager em) { + VariableClass variableClass = new VariableClass().code(UPDATED_CODE).name(UPDATED_NAME).version(UPDATED_VERSION); + return variableClass; + } + + @BeforeEach + public void initTest() { + variableClass = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedVariableClass != null) { + variableClassRepository.delete(insertedVariableClass); + insertedVariableClass = null; + } + } + + @Test + @Transactional + void createVariableClass() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the VariableClass + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + var returnedVariableClassDTO = om.readValue( + restVariableClassMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + VariableClassDTO.class + ); + + // Validate the VariableClass in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedVariableClass = variableClassMapper.toEntity(returnedVariableClassDTO); + assertVariableClassUpdatableFieldsEquals(returnedVariableClass, getPersistedVariableClass(returnedVariableClass)); + + insertedVariableClass = returnedVariableClass; + } + + @Test + @Transactional + void createVariableClassWithExistingId() throws Exception { + // Create the VariableClass with an existing ID + variableClass.setId(1L); + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restVariableClassMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableClass.setCode(null); + + // Create the VariableClass, which fails. + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + restVariableClassMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableClass.setName(null); + + // Create the VariableClass, which fails. + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + restVariableClassMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllVariableClasses() throws Exception { + // Initialize the database + insertedVariableClass = variableClassRepository.saveAndFlush(variableClass); + + // Get all the variableClassList + restVariableClassMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(variableClass.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariableClassesWithEagerRelationshipsIsEnabled() throws Exception { + when(variableClassServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableClassMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(variableClassServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariableClassesWithEagerRelationshipsIsNotEnabled() throws Exception { + when(variableClassServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableClassMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(variableClassRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getVariableClass() throws Exception { + // Initialize the database + insertedVariableClass = variableClassRepository.saveAndFlush(variableClass); + + // Get the variableClass + restVariableClassMockMvc + .perform(get(ENTITY_API_URL_ID, variableClass.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(variableClass.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingVariableClass() throws Exception { + // Get the variableClass + restVariableClassMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingVariableClass() throws Exception { + // Initialize the database + insertedVariableClass = variableClassRepository.saveAndFlush(variableClass); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableClass + VariableClass updatedVariableClass = variableClassRepository.findById(variableClass.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedVariableClass are not directly saved in db + em.detach(updatedVariableClass); + updatedVariableClass.code(UPDATED_CODE).name(UPDATED_NAME).version(UPDATED_VERSION); + VariableClassDTO variableClassDTO = variableClassMapper.toDto(updatedVariableClass); + + restVariableClassMockMvc + .perform( + put(ENTITY_API_URL_ID, variableClassDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isOk()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedVariableClassToMatchAllProperties(updatedVariableClass); + } + + @Test + @Transactional + void putNonExistingVariableClass() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableClass.setId(longCount.incrementAndGet()); + + // Create the VariableClass + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableClassMockMvc + .perform( + put(ENTITY_API_URL_ID, variableClassDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchVariableClass() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableClass.setId(longCount.incrementAndGet()); + + // Create the VariableClass + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableClassMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamVariableClass() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableClass.setId(longCount.incrementAndGet()); + + // Create the VariableClass + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableClassMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateVariableClassWithPatch() throws Exception { + // Initialize the database + insertedVariableClass = variableClassRepository.saveAndFlush(variableClass); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableClass using partial update + VariableClass partialUpdatedVariableClass = new VariableClass(); + partialUpdatedVariableClass.setId(variableClass.getId()); + + partialUpdatedVariableClass.code(UPDATED_CODE).name(UPDATED_NAME); + + restVariableClassMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableClass.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableClass)) + ) + .andExpect(status().isOk()); + + // Validate the VariableClass in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableClassUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedVariableClass, variableClass), + getPersistedVariableClass(variableClass) + ); + } + + @Test + @Transactional + void fullUpdateVariableClassWithPatch() throws Exception { + // Initialize the database + insertedVariableClass = variableClassRepository.saveAndFlush(variableClass); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableClass using partial update + VariableClass partialUpdatedVariableClass = new VariableClass(); + partialUpdatedVariableClass.setId(variableClass.getId()); + + partialUpdatedVariableClass.code(UPDATED_CODE).name(UPDATED_NAME).version(UPDATED_VERSION); + + restVariableClassMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableClass.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableClass)) + ) + .andExpect(status().isOk()); + + // Validate the VariableClass in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableClassUpdatableFieldsEquals(partialUpdatedVariableClass, getPersistedVariableClass(partialUpdatedVariableClass)); + } + + @Test + @Transactional + void patchNonExistingVariableClass() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableClass.setId(longCount.incrementAndGet()); + + // Create the VariableClass + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableClassMockMvc + .perform( + patch(ENTITY_API_URL_ID, variableClassDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchVariableClass() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableClass.setId(longCount.incrementAndGet()); + + // Create the VariableClass + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableClassMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamVariableClass() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableClass.setId(longCount.incrementAndGet()); + + // Create the VariableClass + VariableClassDTO variableClassDTO = variableClassMapper.toDto(variableClass); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableClassMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableClassDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableClass in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteVariableClass() throws Exception { + // Initialize the database + insertedVariableClass = variableClassRepository.saveAndFlush(variableClass); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the variableClass + restVariableClassMockMvc + .perform(delete(ENTITY_API_URL_ID, variableClass.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return variableClassRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected VariableClass getPersistedVariableClass(VariableClass variableClass) { + return variableClassRepository.findById(variableClass.getId()).orElseThrow(); + } + + protected void assertPersistedVariableClassToMatchAllProperties(VariableClass expectedVariableClass) { + assertVariableClassAllPropertiesEquals(expectedVariableClass, getPersistedVariableClass(expectedVariableClass)); + } + + protected void assertPersistedVariableClassToMatchUpdatableProperties(VariableClass expectedVariableClass) { + assertVariableClassAllUpdatablePropertiesEquals(expectedVariableClass, getPersistedVariableClass(expectedVariableClass)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/VariableResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/VariableResourceIT.java new file mode 100644 index 0000000..b50eadc --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/VariableResourceIT.java @@ -0,0 +1,1264 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.VariableAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.Unit; +import com.oguerreiro.resilient.domain.Variable; +import com.oguerreiro.resilient.domain.VariableCategory; +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.domain.enumeration.InputMode; +import com.oguerreiro.resilient.repository.VariableRepository; +import com.oguerreiro.resilient.service.VariableService; +import com.oguerreiro.resilient.service.dto.VariableDTO; +import com.oguerreiro.resilient.service.mapper.VariableMapper; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link VariableResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class VariableResourceIT { + + private static final String DEFAULT_CODE = ""; + private static final String UPDATED_CODE = "1"; + + private static final Integer DEFAULT_VARIABLE_INDEX = 1; + private static final Integer UPDATED_VARIABLE_INDEX = 2; + private static final Integer SMALLER_VARIABLE_INDEX = 1 - 1; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final Boolean DEFAULT_INPUT = false; + private static final Boolean UPDATED_INPUT = true; + + private static final InputMode DEFAULT_INPUT_MODE = InputMode.DATAFILE; + private static final InputMode UPDATED_INPUT_MODE = InputMode.MANUAL; + + private static final Boolean DEFAULT_OUTPUT = false; + private static final Boolean UPDATED_OUTPUT = true; + + private static final String DEFAULT_OUTPUT_FORMULA = "AAAAAAAAAA"; + private static final String UPDATED_OUTPUT_FORMULA = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + private static final Integer SMALLER_VERSION = 1 - 1; + + private static final String ENTITY_API_URL = "/api/variables"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private VariableRepository variableRepository; + + @Mock + private VariableRepository variableRepositoryMock; + + @Autowired + private VariableMapper variableMapper; + + @Mock + private VariableService variableServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restVariableMockMvc; + + private Variable variable; + + private Variable insertedVariable; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Variable createEntity(EntityManager em) { + Variable variable = new Variable() + .code(DEFAULT_CODE) + .variableIndex(DEFAULT_VARIABLE_INDEX) + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .input(DEFAULT_INPUT) + .inputMode(DEFAULT_INPUT_MODE) + .output(DEFAULT_OUTPUT) + .outputFormula(DEFAULT_OUTPUT_FORMULA) + .version(DEFAULT_VERSION); + return variable; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static Variable createUpdatedEntity(EntityManager em) { + Variable variable = new Variable() + .code(UPDATED_CODE) + .variableIndex(UPDATED_VARIABLE_INDEX) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .input(UPDATED_INPUT) + .inputMode(UPDATED_INPUT_MODE) + .output(UPDATED_OUTPUT) + .outputFormula(UPDATED_OUTPUT_FORMULA) + .version(UPDATED_VERSION); + return variable; + } + + @BeforeEach + public void initTest() { + variable = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedVariable != null) { + variableRepository.delete(insertedVariable); + insertedVariable = null; + } + } + + @Test + @Transactional + void createVariable() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the Variable + VariableDTO variableDTO = variableMapper.toDto(variable); + var returnedVariableDTO = om.readValue( + restVariableMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + VariableDTO.class + ); + + // Validate the Variable in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedVariable = variableMapper.toEntity(returnedVariableDTO); + assertVariableUpdatableFieldsEquals(returnedVariable, getPersistedVariable(returnedVariable)); + + insertedVariable = returnedVariable; + } + + @Test + @Transactional + void createVariableWithExistingId() throws Exception { + // Create the Variable with an existing ID + variable.setId(1L); + VariableDTO variableDTO = variableMapper.toDto(variable); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variable.setCode(null); + + // Create the Variable, which fails. + VariableDTO variableDTO = variableMapper.toDto(variable); + + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkVariableIndexIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variable.setVariableIndex(null); + + // Create the Variable, which fails. + VariableDTO variableDTO = variableMapper.toDto(variable); + + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variable.setName(null); + + // Create the Variable, which fails. + VariableDTO variableDTO = variableMapper.toDto(variable); + + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkDescriptionIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variable.setDescription(null); + + // Create the Variable, which fails. + VariableDTO variableDTO = variableMapper.toDto(variable); + + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkInputIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variable.setInput(null); + + // Create the Variable, which fails. + VariableDTO variableDTO = variableMapper.toDto(variable); + + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkInputModeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variable.setInputMode(null); + + // Create the Variable, which fails. + VariableDTO variableDTO = variableMapper.toDto(variable); + + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkOutputIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variable.setOutput(null); + + // Create the Variable, which fails. + VariableDTO variableDTO = variableMapper.toDto(variable); + + restVariableMockMvc + .perform(post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllVariables() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList + restVariableMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(variable.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].variableIndex").value(hasItem(DEFAULT_VARIABLE_INDEX))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].input").value(hasItem(DEFAULT_INPUT.booleanValue()))) + .andExpect(jsonPath("$.[*].inputMode").value(hasItem(DEFAULT_INPUT_MODE.toString()))) + .andExpect(jsonPath("$.[*].output").value(hasItem(DEFAULT_OUTPUT.booleanValue()))) + .andExpect(jsonPath("$.[*].outputFormula").value(hasItem(DEFAULT_OUTPUT_FORMULA))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariablesWithEagerRelationshipsIsEnabled() throws Exception { + when(variableServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(variableServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariablesWithEagerRelationshipsIsNotEnabled() throws Exception { + when(variableServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(variableRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getVariable() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get the variable + restVariableMockMvc + .perform(get(ENTITY_API_URL_ID, variable.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(variable.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.variableIndex").value(DEFAULT_VARIABLE_INDEX)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.input").value(DEFAULT_INPUT.booleanValue())) + .andExpect(jsonPath("$.inputMode").value(DEFAULT_INPUT_MODE.toString())) + .andExpect(jsonPath("$.output").value(DEFAULT_OUTPUT.booleanValue())) + .andExpect(jsonPath("$.outputFormula").value(DEFAULT_OUTPUT_FORMULA)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getVariablesByIdFiltering() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + Long id = variable.getId(); + + defaultVariableFiltering("id.equals=" + id, "id.notEquals=" + id); + + defaultVariableFiltering("id.greaterThanOrEqual=" + id, "id.greaterThan=" + id); + + defaultVariableFiltering("id.lessThanOrEqual=" + id, "id.lessThan=" + id); + } + + @Test + @Transactional + void getAllVariablesByCodeIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where code equals to + defaultVariableFiltering("code.equals=" + DEFAULT_CODE, "code.equals=" + UPDATED_CODE); + } + + @Test + @Transactional + void getAllVariablesByCodeIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where code in + defaultVariableFiltering("code.in=" + DEFAULT_CODE + "," + UPDATED_CODE, "code.in=" + UPDATED_CODE); + } + + @Test + @Transactional + void getAllVariablesByCodeIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where code is not null + defaultVariableFiltering("code.specified=true", "code.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByCodeContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where code contains + defaultVariableFiltering("code.contains=" + DEFAULT_CODE, "code.contains=" + UPDATED_CODE); + } + + @Test + @Transactional + void getAllVariablesByCodeNotContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where code does not contain + defaultVariableFiltering("code.doesNotContain=" + UPDATED_CODE, "code.doesNotContain=" + DEFAULT_CODE); + } + + @Test + @Transactional + void getAllVariablesByVariableIndexIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where variableIndex equals to + defaultVariableFiltering("variableIndex.equals=" + DEFAULT_VARIABLE_INDEX, "variableIndex.equals=" + UPDATED_VARIABLE_INDEX); + } + + @Test + @Transactional + void getAllVariablesByVariableIndexIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where variableIndex in + defaultVariableFiltering( + "variableIndex.in=" + DEFAULT_VARIABLE_INDEX + "," + UPDATED_VARIABLE_INDEX, + "variableIndex.in=" + UPDATED_VARIABLE_INDEX + ); + } + + @Test + @Transactional + void getAllVariablesByVariableIndexIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where variableIndex is not null + defaultVariableFiltering("variableIndex.specified=true", "variableIndex.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByVariableIndexIsGreaterThanOrEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where variableIndex is greater than or equal to + defaultVariableFiltering( + "variableIndex.greaterThanOrEqual=" + DEFAULT_VARIABLE_INDEX, + "variableIndex.greaterThanOrEqual=" + UPDATED_VARIABLE_INDEX + ); + } + + @Test + @Transactional + void getAllVariablesByVariableIndexIsLessThanOrEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where variableIndex is less than or equal to + defaultVariableFiltering( + "variableIndex.lessThanOrEqual=" + DEFAULT_VARIABLE_INDEX, + "variableIndex.lessThanOrEqual=" + SMALLER_VARIABLE_INDEX + ); + } + + @Test + @Transactional + void getAllVariablesByVariableIndexIsLessThanSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where variableIndex is less than + defaultVariableFiltering("variableIndex.lessThan=" + UPDATED_VARIABLE_INDEX, "variableIndex.lessThan=" + DEFAULT_VARIABLE_INDEX); + } + + @Test + @Transactional + void getAllVariablesByVariableIndexIsGreaterThanSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where variableIndex is greater than + defaultVariableFiltering( + "variableIndex.greaterThan=" + SMALLER_VARIABLE_INDEX, + "variableIndex.greaterThan=" + DEFAULT_VARIABLE_INDEX + ); + } + + @Test + @Transactional + void getAllVariablesByNameIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where name equals to + defaultVariableFiltering("name.equals=" + DEFAULT_NAME, "name.equals=" + UPDATED_NAME); + } + + @Test + @Transactional + void getAllVariablesByNameIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where name in + defaultVariableFiltering("name.in=" + DEFAULT_NAME + "," + UPDATED_NAME, "name.in=" + UPDATED_NAME); + } + + @Test + @Transactional + void getAllVariablesByNameIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where name is not null + defaultVariableFiltering("name.specified=true", "name.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByNameContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where name contains + defaultVariableFiltering("name.contains=" + DEFAULT_NAME, "name.contains=" + UPDATED_NAME); + } + + @Test + @Transactional + void getAllVariablesByNameNotContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where name does not contain + defaultVariableFiltering("name.doesNotContain=" + UPDATED_NAME, "name.doesNotContain=" + DEFAULT_NAME); + } + + @Test + @Transactional + void getAllVariablesByDescriptionIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where description equals to + defaultVariableFiltering("description.equals=" + DEFAULT_DESCRIPTION, "description.equals=" + UPDATED_DESCRIPTION); + } + + @Test + @Transactional + void getAllVariablesByDescriptionIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where description in + defaultVariableFiltering( + "description.in=" + DEFAULT_DESCRIPTION + "," + UPDATED_DESCRIPTION, + "description.in=" + UPDATED_DESCRIPTION + ); + } + + @Test + @Transactional + void getAllVariablesByDescriptionIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where description is not null + defaultVariableFiltering("description.specified=true", "description.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByDescriptionContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where description contains + defaultVariableFiltering("description.contains=" + DEFAULT_DESCRIPTION, "description.contains=" + UPDATED_DESCRIPTION); + } + + @Test + @Transactional + void getAllVariablesByDescriptionNotContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where description does not contain + defaultVariableFiltering("description.doesNotContain=" + UPDATED_DESCRIPTION, "description.doesNotContain=" + DEFAULT_DESCRIPTION); + } + + @Test + @Transactional + void getAllVariablesByInputIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where input equals to + defaultVariableFiltering("input.equals=" + DEFAULT_INPUT, "input.equals=" + UPDATED_INPUT); + } + + @Test + @Transactional + void getAllVariablesByInputIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where input in + defaultVariableFiltering("input.in=" + DEFAULT_INPUT + "," + UPDATED_INPUT, "input.in=" + UPDATED_INPUT); + } + + @Test + @Transactional + void getAllVariablesByInputIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where input is not null + defaultVariableFiltering("input.specified=true", "input.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByInputModeIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where inputMode equals to + defaultVariableFiltering("inputMode.equals=" + DEFAULT_INPUT_MODE, "inputMode.equals=" + UPDATED_INPUT_MODE); + } + + @Test + @Transactional + void getAllVariablesByInputModeIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where inputMode in + defaultVariableFiltering("inputMode.in=" + DEFAULT_INPUT_MODE + "," + UPDATED_INPUT_MODE, "inputMode.in=" + UPDATED_INPUT_MODE); + } + + @Test + @Transactional + void getAllVariablesByInputModeIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where inputMode is not null + defaultVariableFiltering("inputMode.specified=true", "inputMode.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByOutputIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where output equals to + defaultVariableFiltering("output.equals=" + DEFAULT_OUTPUT, "output.equals=" + UPDATED_OUTPUT); + } + + @Test + @Transactional + void getAllVariablesByOutputIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where output in + defaultVariableFiltering("output.in=" + DEFAULT_OUTPUT + "," + UPDATED_OUTPUT, "output.in=" + UPDATED_OUTPUT); + } + + @Test + @Transactional + void getAllVariablesByOutputIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where output is not null + defaultVariableFiltering("output.specified=true", "output.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByOutputFormulaIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where outputFormula equals to + defaultVariableFiltering("outputFormula.equals=" + DEFAULT_OUTPUT_FORMULA, "outputFormula.equals=" + UPDATED_OUTPUT_FORMULA); + } + + @Test + @Transactional + void getAllVariablesByOutputFormulaIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where outputFormula in + defaultVariableFiltering( + "outputFormula.in=" + DEFAULT_OUTPUT_FORMULA + "," + UPDATED_OUTPUT_FORMULA, + "outputFormula.in=" + UPDATED_OUTPUT_FORMULA + ); + } + + @Test + @Transactional + void getAllVariablesByOutputFormulaIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where outputFormula is not null + defaultVariableFiltering("outputFormula.specified=true", "outputFormula.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByOutputFormulaContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where outputFormula contains + defaultVariableFiltering("outputFormula.contains=" + DEFAULT_OUTPUT_FORMULA, "outputFormula.contains=" + UPDATED_OUTPUT_FORMULA); + } + + @Test + @Transactional + void getAllVariablesByOutputFormulaNotContainsSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where outputFormula does not contain + defaultVariableFiltering( + "outputFormula.doesNotContain=" + UPDATED_OUTPUT_FORMULA, + "outputFormula.doesNotContain=" + DEFAULT_OUTPUT_FORMULA + ); + } + + @Test + @Transactional + void getAllVariablesByVersionIsEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where version equals to + defaultVariableFiltering("version.equals=" + DEFAULT_VERSION, "version.equals=" + UPDATED_VERSION); + } + + @Test + @Transactional + void getAllVariablesByVersionIsInShouldWork() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where version in + defaultVariableFiltering("version.in=" + DEFAULT_VERSION + "," + UPDATED_VERSION, "version.in=" + UPDATED_VERSION); + } + + @Test + @Transactional + void getAllVariablesByVersionIsNullOrNotNull() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where version is not null + defaultVariableFiltering("version.specified=true", "version.specified=false"); + } + + @Test + @Transactional + void getAllVariablesByVersionIsGreaterThanOrEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where version is greater than or equal to + defaultVariableFiltering("version.greaterThanOrEqual=" + DEFAULT_VERSION, "version.greaterThanOrEqual=" + UPDATED_VERSION); + } + + @Test + @Transactional + void getAllVariablesByVersionIsLessThanOrEqualToSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where version is less than or equal to + defaultVariableFiltering("version.lessThanOrEqual=" + DEFAULT_VERSION, "version.lessThanOrEqual=" + SMALLER_VERSION); + } + + @Test + @Transactional + void getAllVariablesByVersionIsLessThanSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where version is less than + defaultVariableFiltering("version.lessThan=" + UPDATED_VERSION, "version.lessThan=" + DEFAULT_VERSION); + } + + @Test + @Transactional + void getAllVariablesByVersionIsGreaterThanSomething() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + // Get all the variableList where version is greater than + defaultVariableFiltering("version.greaterThan=" + SMALLER_VERSION, "version.greaterThan=" + DEFAULT_VERSION); + } + + @Test + @Transactional + void getAllVariablesByBaseUnitIsEqualToSomething() throws Exception { + Unit baseUnit; + if (TestUtil.findAll(em, Unit.class).isEmpty()) { + variableRepository.saveAndFlush(variable); + baseUnit = UnitResourceIT.createEntity(em); + } else { + baseUnit = TestUtil.findAll(em, Unit.class).get(0); + } + em.persist(baseUnit); + em.flush(); + variable.setBaseUnit(baseUnit); + variableRepository.saveAndFlush(variable); + Long baseUnitId = baseUnit.getId(); + // Get all the variableList where baseUnit equals to baseUnitId + defaultVariableShouldBeFound("baseUnitId.equals=" + baseUnitId); + + // Get all the variableList where baseUnit equals to (baseUnitId + 1) + defaultVariableShouldNotBeFound("baseUnitId.equals=" + (baseUnitId + 1)); + } + + @Test + @Transactional + void getAllVariablesByVariableScopeIsEqualToSomething() throws Exception { + VariableScope variableScope; + if (TestUtil.findAll(em, VariableScope.class).isEmpty()) { + variableRepository.saveAndFlush(variable); + variableScope = VariableScopeResourceIT.createEntity(em); + } else { + variableScope = TestUtil.findAll(em, VariableScope.class).get(0); + } + em.persist(variableScope); + em.flush(); + variable.setVariableScope(variableScope); + variableRepository.saveAndFlush(variable); + Long variableScopeId = variableScope.getId(); + // Get all the variableList where variableScope equals to variableScopeId + defaultVariableShouldBeFound("variableScopeId.equals=" + variableScopeId); + + // Get all the variableList where variableScope equals to (variableScopeId + 1) + defaultVariableShouldNotBeFound("variableScopeId.equals=" + (variableScopeId + 1)); + } + + @Test + @Transactional + void getAllVariablesByVariableCategoryIsEqualToSomething() throws Exception { + VariableCategory variableCategory; + if (TestUtil.findAll(em, VariableCategory.class).isEmpty()) { + variableRepository.saveAndFlush(variable); + variableCategory = VariableCategoryResourceIT.createEntity(em); + } else { + variableCategory = TestUtil.findAll(em, VariableCategory.class).get(0); + } + em.persist(variableCategory); + em.flush(); + variable.setVariableCategory(variableCategory); + variableRepository.saveAndFlush(variable); + Long variableCategoryId = variableCategory.getId(); + // Get all the variableList where variableCategory equals to variableCategoryId + defaultVariableShouldBeFound("variableCategoryId.equals=" + variableCategoryId); + + // Get all the variableList where variableCategory equals to (variableCategoryId + 1) + defaultVariableShouldNotBeFound("variableCategoryId.equals=" + (variableCategoryId + 1)); + } + + private void defaultVariableFiltering(String shouldBeFound, String shouldNotBeFound) throws Exception { + defaultVariableShouldBeFound(shouldBeFound); + defaultVariableShouldNotBeFound(shouldNotBeFound); + } + + /** + * Executes the search, and checks that the default entity is returned. + */ + private void defaultVariableShouldBeFound(String filter) throws Exception { + restVariableMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(variable.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].variableIndex").value(hasItem(DEFAULT_VARIABLE_INDEX))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].input").value(hasItem(DEFAULT_INPUT.booleanValue()))) + .andExpect(jsonPath("$.[*].inputMode").value(hasItem(DEFAULT_INPUT_MODE.toString()))) + .andExpect(jsonPath("$.[*].output").value(hasItem(DEFAULT_OUTPUT.booleanValue()))) + .andExpect(jsonPath("$.[*].outputFormula").value(hasItem(DEFAULT_OUTPUT_FORMULA))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + + // Check, that the count call also returns 1 + restVariableMockMvc + .perform(get(ENTITY_API_URL + "/count?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().string("1")); + } + + /** + * Executes the search, and checks that the default entity is not returned. + */ + private void defaultVariableShouldNotBeFound(String filter) throws Exception { + restVariableMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + + // Check, that the count call also returns 0 + restVariableMockMvc + .perform(get(ENTITY_API_URL + "/count?sort=id,desc&" + filter)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().string("0")); + } + + @Test + @Transactional + void getNonExistingVariable() throws Exception { + // Get the variable + restVariableMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingVariable() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variable + Variable updatedVariable = variableRepository.findById(variable.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedVariable are not directly saved in db + em.detach(updatedVariable); + updatedVariable + .code(UPDATED_CODE) + .variableIndex(UPDATED_VARIABLE_INDEX) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .input(UPDATED_INPUT) + .inputMode(UPDATED_INPUT_MODE) + .output(UPDATED_OUTPUT) + .outputFormula(UPDATED_OUTPUT_FORMULA) + .version(UPDATED_VERSION); + VariableDTO variableDTO = variableMapper.toDto(updatedVariable); + + restVariableMockMvc + .perform( + put(ENTITY_API_URL_ID, variableDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableDTO)) + ) + .andExpect(status().isOk()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedVariableToMatchAllProperties(updatedVariable); + } + + @Test + @Transactional + void putNonExistingVariable() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variable.setId(longCount.incrementAndGet()); + + // Create the Variable + VariableDTO variableDTO = variableMapper.toDto(variable); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableMockMvc + .perform( + put(ENTITY_API_URL_ID, variableDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchVariable() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variable.setId(longCount.incrementAndGet()); + + // Create the Variable + VariableDTO variableDTO = variableMapper.toDto(variable); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamVariable() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variable.setId(longCount.incrementAndGet()); + + // Create the Variable + VariableDTO variableDTO = variableMapper.toDto(variable); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableMockMvc + .perform(put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableDTO))) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateVariableWithPatch() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variable using partial update + Variable partialUpdatedVariable = new Variable(); + partialUpdatedVariable.setId(variable.getId()); + + partialUpdatedVariable + .variableIndex(UPDATED_VARIABLE_INDEX) + .description(UPDATED_DESCRIPTION) + .input(UPDATED_INPUT) + .output(UPDATED_OUTPUT) + .outputFormula(UPDATED_OUTPUT_FORMULA); + + restVariableMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariable.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariable)) + ) + .andExpect(status().isOk()); + + // Validate the Variable in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableUpdatableFieldsEquals(createUpdateProxyForBean(partialUpdatedVariable, variable), getPersistedVariable(variable)); + } + + @Test + @Transactional + void fullUpdateVariableWithPatch() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variable using partial update + Variable partialUpdatedVariable = new Variable(); + partialUpdatedVariable.setId(variable.getId()); + + partialUpdatedVariable + .code(UPDATED_CODE) + .variableIndex(UPDATED_VARIABLE_INDEX) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .input(UPDATED_INPUT) + .inputMode(UPDATED_INPUT_MODE) + .output(UPDATED_OUTPUT) + .outputFormula(UPDATED_OUTPUT_FORMULA) + .version(UPDATED_VERSION); + + restVariableMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariable.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariable)) + ) + .andExpect(status().isOk()); + + // Validate the Variable in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableUpdatableFieldsEquals(partialUpdatedVariable, getPersistedVariable(partialUpdatedVariable)); + } + + @Test + @Transactional + void patchNonExistingVariable() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variable.setId(longCount.incrementAndGet()); + + // Create the Variable + VariableDTO variableDTO = variableMapper.toDto(variable); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableMockMvc + .perform( + patch(ENTITY_API_URL_ID, variableDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchVariable() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variable.setId(longCount.incrementAndGet()); + + // Create the Variable + VariableDTO variableDTO = variableMapper.toDto(variable); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamVariable() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variable.setId(longCount.incrementAndGet()); + + // Create the Variable + VariableDTO variableDTO = variableMapper.toDto(variable); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableMockMvc + .perform( + patch(ENTITY_API_URL).with(csrf()).contentType("application/merge-patch+json").content(om.writeValueAsBytes(variableDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the Variable in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteVariable() throws Exception { + // Initialize the database + insertedVariable = variableRepository.saveAndFlush(variable); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the variable + restVariableMockMvc + .perform(delete(ENTITY_API_URL_ID, variable.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return variableRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected Variable getPersistedVariable(Variable variable) { + return variableRepository.findById(variable.getId()).orElseThrow(); + } + + protected void assertPersistedVariableToMatchAllProperties(Variable expectedVariable) { + assertVariableAllPropertiesEquals(expectedVariable, getPersistedVariable(expectedVariable)); + } + + protected void assertPersistedVariableToMatchUpdatableProperties(Variable expectedVariable) { + assertVariableAllUpdatablePropertiesEquals(expectedVariable, getPersistedVariable(expectedVariable)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/VariableScopeResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/VariableScopeResourceIT.java new file mode 100644 index 0000000..29b27b7 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/VariableScopeResourceIT.java @@ -0,0 +1,536 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.VariableScopeAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.VariableScope; +import com.oguerreiro.resilient.repository.VariableScopeRepository; +import com.oguerreiro.resilient.service.dto.VariableScopeDTO; +import com.oguerreiro.resilient.service.mapper.VariableScopeMapper; +import jakarta.persistence.EntityManager; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link VariableScopeResource} REST controller. + */ +@IntegrationTest +@AutoConfigureMockMvc +@WithMockUser +class VariableScopeResourceIT { + + private static final String DEFAULT_CODE = "98"; + private static final String UPDATED_CODE = "33"; + + private static final String DEFAULT_NAME = "AAAAAAAAAA"; + private static final String UPDATED_NAME = "BBBBBBBBBB"; + + private static final String DEFAULT_DESCRIPTION = "AAAAAAAAAA"; + private static final String UPDATED_DESCRIPTION = "BBBBBBBBBB"; + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/variable-scopes"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private VariableScopeRepository variableScopeRepository; + + @Autowired + private VariableScopeMapper variableScopeMapper; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restVariableScopeMockMvc; + + private VariableScope variableScope; + + private VariableScope insertedVariableScope; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableScope createEntity(EntityManager em) { + VariableScope variableScope = new VariableScope() + .code(DEFAULT_CODE) + .name(DEFAULT_NAME) + .description(DEFAULT_DESCRIPTION) + .version(DEFAULT_VERSION); + return variableScope; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableScope createUpdatedEntity(EntityManager em) { + VariableScope variableScope = new VariableScope() + .code(UPDATED_CODE) + .name(UPDATED_NAME) + .description(UPDATED_DESCRIPTION) + .version(UPDATED_VERSION); + return variableScope; + } + + @BeforeEach + public void initTest() { + variableScope = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedVariableScope != null) { + variableScopeRepository.delete(insertedVariableScope); + insertedVariableScope = null; + } + } + + @Test + @Transactional + void createVariableScope() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the VariableScope + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + var returnedVariableScopeDTO = om.readValue( + restVariableScopeMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + VariableScopeDTO.class + ); + + // Validate the VariableScope in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedVariableScope = variableScopeMapper.toEntity(returnedVariableScopeDTO); + assertVariableScopeUpdatableFieldsEquals(returnedVariableScope, getPersistedVariableScope(returnedVariableScope)); + + insertedVariableScope = returnedVariableScope; + } + + @Test + @Transactional + void createVariableScopeWithExistingId() throws Exception { + // Create the VariableScope with an existing ID + variableScope.setId(1L); + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restVariableScopeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void checkCodeIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableScope.setCode(null); + + // Create the VariableScope, which fails. + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + restVariableScopeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkNameIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableScope.setName(null); + + // Create the VariableScope, which fails. + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + restVariableScopeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void checkDescriptionIsRequired() throws Exception { + long databaseSizeBeforeTest = getRepositoryCount(); + // set the field null + variableScope.setDescription(null); + + // Create the VariableScope, which fails. + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + restVariableScopeMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + assertSameRepositoryCount(databaseSizeBeforeTest); + } + + @Test + @Transactional + void getAllVariableScopes() throws Exception { + // Initialize the database + insertedVariableScope = variableScopeRepository.saveAndFlush(variableScope); + + // Get all the variableScopeList + restVariableScopeMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(variableScope.getId().intValue()))) + .andExpect(jsonPath("$.[*].code").value(hasItem(DEFAULT_CODE))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) + .andExpect(jsonPath("$.[*].description").value(hasItem(DEFAULT_DESCRIPTION))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @Test + @Transactional + void getVariableScope() throws Exception { + // Initialize the database + insertedVariableScope = variableScopeRepository.saveAndFlush(variableScope); + + // Get the variableScope + restVariableScopeMockMvc + .perform(get(ENTITY_API_URL_ID, variableScope.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(variableScope.getId().intValue())) + .andExpect(jsonPath("$.code").value(DEFAULT_CODE)) + .andExpect(jsonPath("$.name").value(DEFAULT_NAME)) + .andExpect(jsonPath("$.description").value(DEFAULT_DESCRIPTION)) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingVariableScope() throws Exception { + // Get the variableScope + restVariableScopeMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingVariableScope() throws Exception { + // Initialize the database + insertedVariableScope = variableScopeRepository.saveAndFlush(variableScope); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableScope + VariableScope updatedVariableScope = variableScopeRepository.findById(variableScope.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedVariableScope are not directly saved in db + em.detach(updatedVariableScope); + updatedVariableScope.code(UPDATED_CODE).name(UPDATED_NAME).description(UPDATED_DESCRIPTION).version(UPDATED_VERSION); + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(updatedVariableScope); + + restVariableScopeMockMvc + .perform( + put(ENTITY_API_URL_ID, variableScopeDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isOk()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedVariableScopeToMatchAllProperties(updatedVariableScope); + } + + @Test + @Transactional + void putNonExistingVariableScope() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableScope.setId(longCount.incrementAndGet()); + + // Create the VariableScope + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableScopeMockMvc + .perform( + put(ENTITY_API_URL_ID, variableScopeDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchVariableScope() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableScope.setId(longCount.incrementAndGet()); + + // Create the VariableScope + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableScopeMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamVariableScope() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableScope.setId(longCount.incrementAndGet()); + + // Create the VariableScope + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableScopeMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateVariableScopeWithPatch() throws Exception { + // Initialize the database + insertedVariableScope = variableScopeRepository.saveAndFlush(variableScope); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableScope using partial update + VariableScope partialUpdatedVariableScope = new VariableScope(); + partialUpdatedVariableScope.setId(variableScope.getId()); + + partialUpdatedVariableScope.code(UPDATED_CODE).version(UPDATED_VERSION); + + restVariableScopeMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableScope.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableScope)) + ) + .andExpect(status().isOk()); + + // Validate the VariableScope in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableScopeUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedVariableScope, variableScope), + getPersistedVariableScope(variableScope) + ); + } + + @Test + @Transactional + void fullUpdateVariableScopeWithPatch() throws Exception { + // Initialize the database + insertedVariableScope = variableScopeRepository.saveAndFlush(variableScope); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableScope using partial update + VariableScope partialUpdatedVariableScope = new VariableScope(); + partialUpdatedVariableScope.setId(variableScope.getId()); + + partialUpdatedVariableScope.code(UPDATED_CODE).name(UPDATED_NAME).description(UPDATED_DESCRIPTION).version(UPDATED_VERSION); + + restVariableScopeMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableScope.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableScope)) + ) + .andExpect(status().isOk()); + + // Validate the VariableScope in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableScopeUpdatableFieldsEquals(partialUpdatedVariableScope, getPersistedVariableScope(partialUpdatedVariableScope)); + } + + @Test + @Transactional + void patchNonExistingVariableScope() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableScope.setId(longCount.incrementAndGet()); + + // Create the VariableScope + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableScopeMockMvc + .perform( + patch(ENTITY_API_URL_ID, variableScopeDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchVariableScope() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableScope.setId(longCount.incrementAndGet()); + + // Create the VariableScope + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableScopeMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamVariableScope() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableScope.setId(longCount.incrementAndGet()); + + // Create the VariableScope + VariableScopeDTO variableScopeDTO = variableScopeMapper.toDto(variableScope); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableScopeMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableScopeDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableScope in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteVariableScope() throws Exception { + // Initialize the database + insertedVariableScope = variableScopeRepository.saveAndFlush(variableScope); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the variableScope + restVariableScopeMockMvc + .perform(delete(ENTITY_API_URL_ID, variableScope.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return variableScopeRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected VariableScope getPersistedVariableScope(VariableScope variableScope) { + return variableScopeRepository.findById(variableScope.getId()).orElseThrow(); + } + + protected void assertPersistedVariableScopeToMatchAllProperties(VariableScope expectedVariableScope) { + assertVariableScopeAllPropertiesEquals(expectedVariableScope, getPersistedVariableScope(expectedVariableScope)); + } + + protected void assertPersistedVariableScopeToMatchUpdatableProperties(VariableScope expectedVariableScope) { + assertVariableScopeAllUpdatablePropertiesEquals(expectedVariableScope, getPersistedVariableScope(expectedVariableScope)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/VariableUnitsResourceIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/VariableUnitsResourceIT.java new file mode 100644 index 0000000..12cd0c6 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/VariableUnitsResourceIT.java @@ -0,0 +1,486 @@ +package com.oguerreiro.resilient.web.rest; + +import static com.oguerreiro.resilient.domain.VariableUnitsAsserts.*; +import static com.oguerreiro.resilient.web.rest.TestUtil.createUpdateProxyForBean; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oguerreiro.resilient.IntegrationTest; +import com.oguerreiro.resilient.domain.VariableUnits; +import com.oguerreiro.resilient.repository.VariableUnitsRepository; +import com.oguerreiro.resilient.service.VariableUnitsService; +import com.oguerreiro.resilient.service.dto.VariableUnitsDTO; +import com.oguerreiro.resilient.service.mapper.VariableUnitsMapper; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link VariableUnitsResource} REST controller. + */ +@IntegrationTest +@ExtendWith(MockitoExtension.class) +@AutoConfigureMockMvc +@WithMockUser +class VariableUnitsResourceIT { + + private static final Integer DEFAULT_VERSION = 1; + private static final Integer UPDATED_VERSION = 2; + + private static final String ENTITY_API_URL = "/api/variable-units"; + private static final String ENTITY_API_URL_ID = ENTITY_API_URL + "/{id}"; + + private static Random random = new Random(); + private static AtomicLong longCount = new AtomicLong(random.nextInt() + (2 * Integer.MAX_VALUE)); + + @Autowired + private ObjectMapper om; + + @Autowired + private VariableUnitsRepository variableUnitsRepository; + + @Mock + private VariableUnitsRepository variableUnitsRepositoryMock; + + @Autowired + private VariableUnitsMapper variableUnitsMapper; + + @Mock + private VariableUnitsService variableUnitsServiceMock; + + @Autowired + private EntityManager em; + + @Autowired + private MockMvc restVariableUnitsMockMvc; + + private VariableUnits variableUnits; + + private VariableUnits insertedVariableUnits; + + /** + * Create an entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableUnits createEntity(EntityManager em) { + VariableUnits variableUnits = new VariableUnits().version(DEFAULT_VERSION); + return variableUnits; + } + + /** + * Create an updated entity for this test. + * + * This is a static method, as tests for other entities might also need it, + * if they test an entity which requires the current entity. + */ + public static VariableUnits createUpdatedEntity(EntityManager em) { + VariableUnits variableUnits = new VariableUnits().version(UPDATED_VERSION); + return variableUnits; + } + + @BeforeEach + public void initTest() { + variableUnits = createEntity(em); + } + + @AfterEach + public void cleanup() { + if (insertedVariableUnits != null) { + variableUnitsRepository.delete(insertedVariableUnits); + insertedVariableUnits = null; + } + } + + @Test + @Transactional + void createVariableUnits() throws Exception { + long databaseSizeBeforeCreate = getRepositoryCount(); + // Create the VariableUnits + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + var returnedVariableUnitsDTO = om.readValue( + restVariableUnitsMockMvc + .perform( + post(ENTITY_API_URL) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(), + VariableUnitsDTO.class + ); + + // Validate the VariableUnits in the database + assertIncrementedRepositoryCount(databaseSizeBeforeCreate); + var returnedVariableUnits = variableUnitsMapper.toEntity(returnedVariableUnitsDTO); + assertVariableUnitsUpdatableFieldsEquals(returnedVariableUnits, getPersistedVariableUnits(returnedVariableUnits)); + + insertedVariableUnits = returnedVariableUnits; + } + + @Test + @Transactional + void createVariableUnitsWithExistingId() throws Exception { + // Create the VariableUnits with an existing ID + variableUnits.setId(1L); + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + + long databaseSizeBeforeCreate = getRepositoryCount(); + + // An entity with an existing ID cannot be created, so this API call must fail + restVariableUnitsMockMvc + .perform( + post(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeCreate); + } + + @Test + @Transactional + void getAllVariableUnits() throws Exception { + // Initialize the database + insertedVariableUnits = variableUnitsRepository.saveAndFlush(variableUnits); + + // Get all the variableUnitsList + restVariableUnitsMockMvc + .perform(get(ENTITY_API_URL + "?sort=id,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[*].id").value(hasItem(variableUnits.getId().intValue()))) + .andExpect(jsonPath("$.[*].version").value(hasItem(DEFAULT_VERSION))); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariableUnitsWithEagerRelationshipsIsEnabled() throws Exception { + when(variableUnitsServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableUnitsMockMvc.perform(get(ENTITY_API_URL + "?eagerload=true")).andExpect(status().isOk()); + + verify(variableUnitsServiceMock, times(1)).findAllWithEagerRelationships(any()); + } + + @SuppressWarnings({ "unchecked" }) + void getAllVariableUnitsWithEagerRelationshipsIsNotEnabled() throws Exception { + when(variableUnitsServiceMock.findAllWithEagerRelationships(any())).thenReturn(new PageImpl(new ArrayList<>())); + + restVariableUnitsMockMvc.perform(get(ENTITY_API_URL + "?eagerload=false")).andExpect(status().isOk()); + verify(variableUnitsRepositoryMock, times(1)).findAll(any(Pageable.class)); + } + + @Test + @Transactional + void getVariableUnits() throws Exception { + // Initialize the database + insertedVariableUnits = variableUnitsRepository.saveAndFlush(variableUnits); + + // Get the variableUnits + restVariableUnitsMockMvc + .perform(get(ENTITY_API_URL_ID, variableUnits.getId())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id").value(variableUnits.getId().intValue())) + .andExpect(jsonPath("$.version").value(DEFAULT_VERSION)); + } + + @Test + @Transactional + void getNonExistingVariableUnits() throws Exception { + // Get the variableUnits + restVariableUnitsMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + } + + @Test + @Transactional + void putExistingVariableUnits() throws Exception { + // Initialize the database + insertedVariableUnits = variableUnitsRepository.saveAndFlush(variableUnits); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableUnits + VariableUnits updatedVariableUnits = variableUnitsRepository.findById(variableUnits.getId()).orElseThrow(); + // Disconnect from session so that the updates on updatedVariableUnits are not directly saved in db + em.detach(updatedVariableUnits); + updatedVariableUnits.version(UPDATED_VERSION); + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(updatedVariableUnits); + + restVariableUnitsMockMvc + .perform( + put(ENTITY_API_URL_ID, variableUnitsDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isOk()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertPersistedVariableUnitsToMatchAllProperties(updatedVariableUnits); + } + + @Test + @Transactional + void putNonExistingVariableUnits() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableUnits.setId(longCount.incrementAndGet()); + + // Create the VariableUnits + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableUnitsMockMvc + .perform( + put(ENTITY_API_URL_ID, variableUnitsDTO.getId()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithIdMismatchVariableUnits() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableUnits.setId(longCount.incrementAndGet()); + + // Create the VariableUnits + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableUnitsMockMvc + .perform( + put(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void putWithMissingIdPathParamVariableUnits() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableUnits.setId(longCount.incrementAndGet()); + + // Create the VariableUnits + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableUnitsMockMvc + .perform( + put(ENTITY_API_URL).with(csrf()).contentType(MediaType.APPLICATION_JSON).content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void partialUpdateVariableUnitsWithPatch() throws Exception { + // Initialize the database + insertedVariableUnits = variableUnitsRepository.saveAndFlush(variableUnits); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableUnits using partial update + VariableUnits partialUpdatedVariableUnits = new VariableUnits(); + partialUpdatedVariableUnits.setId(variableUnits.getId()); + + restVariableUnitsMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableUnits.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableUnits)) + ) + .andExpect(status().isOk()); + + // Validate the VariableUnits in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableUnitsUpdatableFieldsEquals( + createUpdateProxyForBean(partialUpdatedVariableUnits, variableUnits), + getPersistedVariableUnits(variableUnits) + ); + } + + @Test + @Transactional + void fullUpdateVariableUnitsWithPatch() throws Exception { + // Initialize the database + insertedVariableUnits = variableUnitsRepository.saveAndFlush(variableUnits); + + long databaseSizeBeforeUpdate = getRepositoryCount(); + + // Update the variableUnits using partial update + VariableUnits partialUpdatedVariableUnits = new VariableUnits(); + partialUpdatedVariableUnits.setId(variableUnits.getId()); + + partialUpdatedVariableUnits.version(UPDATED_VERSION); + + restVariableUnitsMockMvc + .perform( + patch(ENTITY_API_URL_ID, partialUpdatedVariableUnits.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(partialUpdatedVariableUnits)) + ) + .andExpect(status().isOk()); + + // Validate the VariableUnits in the database + + assertSameRepositoryCount(databaseSizeBeforeUpdate); + assertVariableUnitsUpdatableFieldsEquals(partialUpdatedVariableUnits, getPersistedVariableUnits(partialUpdatedVariableUnits)); + } + + @Test + @Transactional + void patchNonExistingVariableUnits() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableUnits.setId(longCount.incrementAndGet()); + + // Create the VariableUnits + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + + // If the entity doesn't have an ID, it will throw BadRequestAlertException + restVariableUnitsMockMvc + .perform( + patch(ENTITY_API_URL_ID, variableUnitsDTO.getId()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithIdMismatchVariableUnits() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableUnits.setId(longCount.incrementAndGet()); + + // Create the VariableUnits + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableUnitsMockMvc + .perform( + patch(ENTITY_API_URL_ID, longCount.incrementAndGet()) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isBadRequest()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void patchWithMissingIdPathParamVariableUnits() throws Exception { + long databaseSizeBeforeUpdate = getRepositoryCount(); + variableUnits.setId(longCount.incrementAndGet()); + + // Create the VariableUnits + VariableUnitsDTO variableUnitsDTO = variableUnitsMapper.toDto(variableUnits); + + // If url ID doesn't match entity ID, it will throw BadRequestAlertException + restVariableUnitsMockMvc + .perform( + patch(ENTITY_API_URL) + .with(csrf()) + .contentType("application/merge-patch+json") + .content(om.writeValueAsBytes(variableUnitsDTO)) + ) + .andExpect(status().isMethodNotAllowed()); + + // Validate the VariableUnits in the database + assertSameRepositoryCount(databaseSizeBeforeUpdate); + } + + @Test + @Transactional + void deleteVariableUnits() throws Exception { + // Initialize the database + insertedVariableUnits = variableUnitsRepository.saveAndFlush(variableUnits); + + long databaseSizeBeforeDelete = getRepositoryCount(); + + // Delete the variableUnits + restVariableUnitsMockMvc + .perform(delete(ENTITY_API_URL_ID, variableUnits.getId()).with(csrf()).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Validate the database contains one less item + assertDecrementedRepositoryCount(databaseSizeBeforeDelete); + } + + protected long getRepositoryCount() { + return variableUnitsRepository.count(); + } + + protected void assertIncrementedRepositoryCount(long countBefore) { + assertThat(countBefore + 1).isEqualTo(getRepositoryCount()); + } + + protected void assertDecrementedRepositoryCount(long countBefore) { + assertThat(countBefore - 1).isEqualTo(getRepositoryCount()); + } + + protected void assertSameRepositoryCount(long countBefore) { + assertThat(countBefore).isEqualTo(getRepositoryCount()); + } + + protected VariableUnits getPersistedVariableUnits(VariableUnits variableUnits) { + return variableUnitsRepository.findById(variableUnits.getId()).orElseThrow(); + } + + protected void assertPersistedVariableUnitsToMatchAllProperties(VariableUnits expectedVariableUnits) { + assertVariableUnitsAllPropertiesEquals(expectedVariableUnits, getPersistedVariableUnits(expectedVariableUnits)); + } + + protected void assertPersistedVariableUnitsToMatchUpdatableProperties(VariableUnits expectedVariableUnits) { + assertVariableUnitsAllUpdatablePropertiesEquals(expectedVariableUnits, getPersistedVariableUnits(expectedVariableUnits)); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/WithUnauthenticatedMockUser.java b/src/test/java/com/oguerreiro/resilient/web/rest/WithUnauthenticatedMockUser.java new file mode 100644 index 0000000..e6d3b97 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/WithUnauthenticatedMockUser.java @@ -0,0 +1,23 @@ +package com.oguerreiro.resilient.web.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContext; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithUnauthenticatedMockUser.Factory.class) +public @interface WithUnauthenticatedMockUser { + class Factory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithUnauthenticatedMockUser annotation) { + return SecurityContextHolder.createEmptyContext(); + } + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslatorIT.java b/src/test/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslatorIT.java new file mode 100644 index 0000000..4650f08 --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslatorIT.java @@ -0,0 +1,120 @@ +package com.oguerreiro.resilient.web.rest.errors; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.oguerreiro.resilient.IntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +/** + * Integration tests {@link ExceptionTranslator} controller advice. + */ +@WithMockUser +@AutoConfigureMockMvc +@IntegrationTest +class ExceptionTranslatorIT { + + @Autowired + private MockMvc mockMvc; + + @Test + void testConcurrencyFailure() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/concurrency-failure").with(csrf())) + .andExpect(status().isConflict()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value(ErrorConstants.ERR_CONCURRENCY_FAILURE)); + } + + @Test + void testMethodArgumentNotValid() throws Exception { + mockMvc + .perform( + post("/api/exception-translator-test/method-argument").content("{}").contentType(MediaType.APPLICATION_JSON).with(csrf()) + ) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value(ErrorConstants.ERR_VALIDATION)) + .andExpect(jsonPath("$.fieldErrors.[0].objectName").value("test")) + .andExpect(jsonPath("$.fieldErrors.[0].field").value("test")) + .andExpect(jsonPath("$.fieldErrors.[0].message").value("must not be null")); + } + + @Test + void testMissingServletRequestPartException() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/missing-servlet-request-part").with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.400")); + } + + @Test + void testMissingServletRequestParameterException() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/missing-servlet-request-parameter").with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.400")); + } + + @Test + void testAccessDenied() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/access-denied").with(csrf())) + .andExpect(status().isForbidden()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.403")) + .andExpect(jsonPath("$.detail").value("test access denied!")); + } + + @Test + void testUnauthorized() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/unauthorized").with(csrf())) + .andExpect(status().isUnauthorized()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.401")) + .andExpect(jsonPath("$.path").value("/api/exception-translator-test/unauthorized")) + .andExpect(jsonPath("$.detail").value("test authentication failed!")); + } + + @Test + void testMethodNotSupported() throws Exception { + mockMvc + .perform(post("/api/exception-translator-test/access-denied").with(csrf())) + .andExpect(status().isMethodNotAllowed()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.405")) + .andExpect(jsonPath("$.detail").value("Request method 'POST' is not supported")); + } + + @Test + void testExceptionWithResponseStatus() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/response-status").with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.400")) + .andExpect(jsonPath("$.title").value("test response status")); + } + + @Test + void testInternalServerError() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/internal-server-error").with(csrf())) + .andExpect(status().isInternalServerError()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.500")) + .andExpect(jsonPath("$.title").value("Internal Server Error")); + } +} diff --git a/src/test/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslatorTestController.java b/src/test/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslatorTestController.java new file mode 100644 index 0000000..008825c --- /dev/null +++ b/src/test/java/com/oguerreiro/resilient/web/rest/errors/ExceptionTranslatorTestController.java @@ -0,0 +1,66 @@ +package com.oguerreiro.resilient.web.rest.errors; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/exception-translator-test") +public class ExceptionTranslatorTestController { + + @GetMapping("/concurrency-failure") + public void concurrencyFailure() { + throw new ConcurrencyFailureException("test concurrency failure"); + } + + @PostMapping("/method-argument") + public void methodArgument(@Valid @RequestBody TestDTO testDTO) {} + + @GetMapping("/missing-servlet-request-part") + public void missingServletRequestPartException(@RequestPart("part") String part) {} + + @GetMapping("/missing-servlet-request-parameter") + public void missingServletRequestParameterException(@RequestParam("param") String param) {} + + @GetMapping("/access-denied") + public void accessdenied() { + throw new AccessDeniedException("test access denied!"); + } + + @GetMapping("/unauthorized") + public void unauthorized() { + throw new BadCredentialsException("test authentication failed!"); + } + + @GetMapping("/response-status") + public void exceptionWithResponseStatus() { + throw new TestResponseStatusException(); + } + + @GetMapping("/internal-server-error") + public void internalServerError() { + throw new RuntimeException(); + } + + public static class TestDTO { + + @NotNull + private String test; + + public String getTest() { + return test; + } + + public void setTest(String test) { + this.test = test; + } + } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "test response status") + @SuppressWarnings("serial") + public static class TestResponseStatusException extends RuntimeException {} +} diff --git a/src/test/resources/META-INF/spring.factories b/src/test/resources/META-INF/spring.factories new file mode 100644 index 0000000..a251543 --- /dev/null +++ b/src/test/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.test.context.ContextCustomizerFactory = com.oguerreiro.\ + resilient.config.SqlTestContainersSpringContextCustomizerFactory \ No newline at end of file diff --git a/src/test/resources/config/application-testdev.yml b/src/test/resources/config/application-testdev.yml new file mode 100644 index 0000000..b1b4f5f --- /dev/null +++ b/src/test/resources/config/application-testdev.yml @@ -0,0 +1,43 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration is used for unit/integration tests with testcontainers database containers. +# +# To activate this configuration launch integration tests with the 'testcontainers' profile +# +# More information on database containers: https://www.testcontainers.org/modules/databases/ +# =================================================================== + +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:mysql://localhost:3306/resilient_resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true + username: root + password: root + hikari: + auto-commit: false + poolName: Hikari + maximum-pool-size: 1 + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + jpa: + open-in-view: false + hibernate: + ddl-auto: none + naming: + physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + properties: + hibernate.id.new_generator_mappings: true + hibernate.connection.provider_disables_autocommit: true + hibernate.cache.use_second_level_cache: false + hibernate.cache.use_query_cache: false + hibernate.generate_statistics: false + hibernate.hbm2ddl.auto: none #TODO: temp relief for integration tests, revisit required + hibernate.type.preferred_instant_jdbc_type: TIMESTAMP + hibernate.jdbc.time_zone: UTC + hibernate.timezone.default_storage: NORMALIZE + hibernate.query.fail_on_pagination_over_collection_fetch: true diff --git a/src/test/resources/config/application-testprod.yml b/src/test/resources/config/application-testprod.yml new file mode 100644 index 0000000..6098037 --- /dev/null +++ b/src/test/resources/config/application-testprod.yml @@ -0,0 +1,40 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration is used for unit/integration tests with testcontainers database containers. +# +# To activate this configuration launch integration tests with the 'testcontainers' profile +# +# More information on database containers: https://www.testcontainers.org/modules/databases/ +# =================================================================== + +spring: + datasource: + type: com.zaxxer.hikari.HikariDataSource + hikari: + poolName: Hikari + auto-commit: false + maximum-pool-size: 1 + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + jpa: + open-in-view: false + hibernate: + ddl-auto: none + naming: + physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + properties: + hibernate.id.new_generator_mappings: true + hibernate.connection.provider_disables_autocommit: true + hibernate.cache.use_second_level_cache: false + hibernate.cache.use_query_cache: false + hibernate.generate_statistics: false + hibernate.hbm2ddl.auto: none #TODO: temp relief for integration tests, revisit required + hibernate.type.preferred_instant_jdbc_type: TIMESTAMP + hibernate.jdbc.time_zone: UTC + hibernate.timezone.default_storage: NORMALIZE + hibernate.query.fail_on_pagination_over_collection_fetch: true diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml new file mode 100644 index 0000000..9f31152 --- /dev/null +++ b/src/test/resources/config/application.yml @@ -0,0 +1,86 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration is used for unit/integration tests. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +spring: + application: + name: resilient + # Replace by 'prod, faker' to add the faker context and have sample data loaded in production + liquibase: + contexts: test + jackson: + serialization: + write-durations-as-timestamps: false + mail: + host: localhost + main: + allow-bean-definition-overriding: true + messages: + basename: i18n/messages + task: + execution: + thread-name-prefix: resilient-task- + pool: + core-size: 1 + max-size: 50 + queue-capacity: 10000 + scheduling: + thread-name-prefix: resilient-scheduling- + pool: + size: 20 + thymeleaf: + mode: HTML + +server: + port: 10344 + address: localhost + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== +jhipster: + clientApp: + name: 'resilientApp' + mail: + from: resilient@localhost.com + base-url: http://127.0.0.1:8080 + logging: + # To test json console appender + use-json-format: false + logstash: + enabled: false + host: localhost + port: 5000 + ring-buffer-size: 512 + security: + remember-me: + # security key (this key should be unique for your application, and kept secret) + key: 6436150a69ff50bcf383fbb9d974e2e7bd5c4439beaeef76e87a042d920db55f1f161147c30e01db9fd82117e47db521be8f + +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: +management: + health: + mail: + enabled: false diff --git a/src/test/resources/i18n/messages_en.properties b/src/test/resources/i18n/messages_en.properties new file mode 100644 index 0000000..e11494b --- /dev/null +++ b/src/test/resources/i18n/messages_en.properties @@ -0,0 +1,4 @@ +email.test.title=test title +# Value used for English locale unit test in MailServiceIT +# as this file is loaded instead of real file +email.activation.title=resilient account activation diff --git a/src/test/resources/i18n/messages_pt_PT.properties b/src/test/resources/i18n/messages_pt_PT.properties new file mode 100644 index 0000000..076c3e6 --- /dev/null +++ b/src/test/resources/i18n/messages_pt_PT.properties @@ -0,0 +1,4 @@ +email.test.title=test title +# Value used for English locale unit test in MailServiceIT +# as this file is loaded instead of real file +email.activation.title=ativação resilient \ No newline at end of file diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..092a83d --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,4 @@ +junit.jupiter.execution.timeout.default = 15 s +junit.jupiter.execution.timeout.testable.method.default = 15 s +junit.jupiter.execution.timeout.beforeall.method.default = 60 s +junit.jupiter.testclass.order.default=com.oguerreiro.resilient.config.SpringBootTestClassOrderer diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..1169a85 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/templates/mail/activationEmail.html b/src/test/resources/templates/mail/activationEmail.html new file mode 100644 index 0000000..6db87f6 --- /dev/null +++ b/src/test/resources/templates/mail/activationEmail.html @@ -0,0 +1,19 @@ + + + + JHipster activation + + + +

    Dear

    +

    Your JHipster account has been created, please click on the URL below to activate it:

    +

    + Activation link +

    +

    + Regards, +
    + JHipster. +

    + + diff --git a/src/test/resources/templates/mail/creationEmail.html b/src/test/resources/templates/mail/creationEmail.html new file mode 100644 index 0000000..07075e0 --- /dev/null +++ b/src/test/resources/templates/mail/creationEmail.html @@ -0,0 +1,19 @@ + + + + JHipster creation + + + +

    Dear

    +

    Your JHipster account has been created, please click on the URL below to access it:

    +

    + Login link +

    +

    + Regards, +
    + JHipster. +

    + + diff --git a/src/test/resources/templates/mail/passwordResetEmail.html b/src/test/resources/templates/mail/passwordResetEmail.html new file mode 100644 index 0000000..6ddc5d2 --- /dev/null +++ b/src/test/resources/templates/mail/passwordResetEmail.html @@ -0,0 +1,21 @@ + + + + JHipster password reset + + + +

    Dear

    +

    + For your JHipster account a password reset was requested, please click on the URL below to reset it: +

    +

    + Login link +

    +

    + Regards, +
    + JHipster. +

    + + diff --git a/src/test/resources/templates/mail/testEmail.html b/src/test/resources/templates/mail/testEmail.html new file mode 100644 index 0000000..a4ca16a --- /dev/null +++ b/src/test/resources/templates/mail/testEmail.html @@ -0,0 +1 @@ + diff --git a/ssl/readme b/ssl/readme new file mode 100644 index 0000000..d301bf3 --- /dev/null +++ b/ssl/readme @@ -0,0 +1,3 @@ +This certificates ares destined to be used in ANGULAR DEV only, to enable HTTPS access. + +openssl req -x509 -newkey rsa:2048 -nodes -keyout ssl/server.key -out ssl/server.crt -days 365 -subj "/CN=localhost" \ No newline at end of file diff --git a/ssl/server.crt b/ssl/server.crt new file mode 100644 index 0000000..a86d0ff --- /dev/null +++ b/ssl/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUH4C/i+cutQhXiNHpa2h+Ro68RK0wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUwNTE0NDM1NloXDTI2MDUw +NTE0NDM1NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAorMo1kvAZUjVYlFBdAa4iaCSdflEscBj+T6D61dJTJkG +tKe2Dp+aLwjU1Ts6UJ9qRiDkoOxjCWiDFwl1b9kMaM35xLQKYkf7rj6Tv+NxQVRf +tu4vCpvJm+4Nd0lm/CWoSLEa1ncivL8yHXJVPxSzafYlkGy3Q0v6PcBH9f0gONNJ ++V5zx7bbuRJmHoCiXKu9ysJ/enzorWaGPymnDMb5PB8ar++Sxn6wOIx3AhywQ5P6 +GpBLpVSfGPv5ud4kZyGkx1DwpQsilgiVCH8Ky66csuuMiXy6+hkP4Okwt/2NGtbH +T/YSOCZyoHJGHmHZd7YXnezcQCrNsES9+Tm/uFp9ywIDAQABo1MwUTAdBgNVHQ4E +FgQUmROH/vxmQD33OaPZ0333qJkJhCQwHwYDVR0jBBgwFoAUmROH/vxmQD33OaPZ +0333qJkJhCQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEv5D +tsJR4EJYXKPSibaOAYCrvWg2Ws+jYxHn4qTyBBu6DwCD56cDb6NJQN7M+wsCqe2z +Ai7UuVZbqHN3PD3uaa3q9HU14vUIPFG/X4zgksr4RtdMX2i4cnL5KyHE+RWj+kEv +KEb4AfY1MLXBJiBAmhuC7OSd/V90y1AivVKHVjNB/kGANGBdiOz/laCQ21lhIVqP +W6hISAuB6zTJTIH0Ccgs2UicNFNwZDUdsJ1J+SyB3aWbIJPtdUFbZLPR5/hF7AYb +60ctc4ASO92eDvfeEWDQLlZT9rcuLQIPIoeEcE15LZQIJZQwIiRFfAn4DYGdwhH9 ++J7BFAL5liBdtRtTJA== +-----END CERTIFICATE----- diff --git a/ssl/server.key b/ssl/server.key new file mode 100644 index 0000000..dda1b2b --- /dev/null +++ b/ssl/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCisyjWS8BlSNVi +UUF0BriJoJJ1+USxwGP5PoPrV0lMmQa0p7YOn5ovCNTVOzpQn2pGIOSg7GMJaIMX +CXVv2QxozfnEtApiR/uuPpO/43FBVF+27i8Km8mb7g13SWb8JahIsRrWdyK8vzId +clU/FLNp9iWQbLdDS/o9wEf1/SA400n5XnPHttu5EmYegKJcq73Kwn96fOitZoY/ +KacMxvk8Hxqv75LGfrA4jHcCHLBDk/oakEulVJ8Y+/m53iRnIaTHUPClCyKWCJUI +fwrLrpyy64yJfLr6GQ/g6TC3/Y0a1sdP9hI4JnKgckYeYdl3thed7NxAKs2wRL35 +Ob+4Wn3LAgMBAAECggEACBc2salksOS1T8VQpu9qn9VvUxSJOPmjPxK/xqU5jOiT +L8qjQAjpxur+gngGh1nWFBSwqdv3dNFQq4WZ5Gz7s/6CLCjz1z1TI7AS8tjhmekT +NWqUxkdG+E0XOFZbG7DXtH2yIvYbsR0k5df4q0FhRqSp64usdVlC4W09ZtsmjgzH +dVueYs/niQpygYDWgeaLjP8kiHCB0nOCO/L5sxeZReZUq9gWJE+22py9iRPVOBqu +7SLk2MESo/BoTQsNixgz9JRxzjVgZn37D8sZE1kw0Ez+nOZ1UQZDwZ+MRxPbxDWc +jVkBFlAmHWFkp8XJou7su5tYoyzbXMYR5WOnHS94kQKBgQDJsB2S664GsfUvEkGF +7LU2/Irx4Zp8cIvhDam1HJiPsdf5kU60oo70AXK/cLjSYuSFbQJxzuyyImfANp61 +zw20C/t6AP06n77aBy6xFHGzjkNP59P/9HDigoo5EnCR38ZdbaL3xnKvtgCwX9Vf +sX9jcHGcB4ud2Dkq3Nk28LEXWQKBgQDOg0wNhYa5JW+63InkNwJGHWvTsrmOLOHR +debgGgai7BsYdxi9Hg5vmgdnJTIKqDbgYvGxBK3iW5e9mevirpBIpe+1bk+HCiG0 +RHaJcK12a0pLuL9qO6elAv3Og9dh0Fx4a1qIE9B6St2DHVNqPjJUxi4D7loE57i6 +gfrZTuG9wwKBgERxX5fcItJi0bGHoHSyP0zJpt0ukh5okw9sc4riiWM6FvC6PttN +zvU/CJGuzwmWRbaBp7K3ZduNkWfJIVLRUPUom85rR4P9cebLSS0Oowcr3+vd4DOb +4DL92apEd68JIWfTLNqonYLDYLh5zQ8MTQ6RufwZE2gdWEkTaSMbyv0BAoGAfoj8 +pJxYPEGwADL5WphKEGQemYw9HdxqUUmIuW+4+ULqJ0dyJQ6xkbgzKQQlcVo4pax6 +oxhW4wd0JnyViEOlrJ3gDq8Uk42lsSrx5qzf3FtJHrSItLLwua7njQ5Dfm+I+U/u +HMrnRsdwitIBNmv9suvrbgUxqyfAjKbv6Q7oSrkCgYAeSH1G2XbbZcFcoILt1yhN +ozZI3+KLKe1MWG+/paH00FyigPy2i/ol4EEWkklIbIhgSasLbQzG0Nags3iD8GuV +XMNd6QCgg7p1i32x9ZQNXRKjDmh5w0oHgFhWOdBxhqLD5hCiAwF7Jm14x7fLTi0d +iWBazbr0wqh7RdiblSChsw== +-----END PRIVATE KEY----- diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..099fec0 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./target/out-tsc/app", + "types": ["@angular/localize"] + }, + "files": ["src/main/webapp/main.ts"], + "include": ["src/main/webapp/**/*.d.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e4478e0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "baseUrl": "src/main/webapp/", + "outDir": "./target/out-tsc/root", + "forceConsistentCasingInFileNames": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "useDefineForClassFields": false, + "target": "es2022", + "module": "es2020", + "types": [], + "lib": ["es2018", "es2020", "dom"] + }, + "references": [ + { + "path": "tsconfig.spec.json" + } + ], + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "preserveWhitespaces": true + } +} diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..d00c687 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/main/webapp/**/*.ts"], + "compilerOptions": { + "composite": true, + "outDir": "target/out-tsc/spec", + "types": ["jest", "node"] + } +} diff --git a/webpack/environment.js b/webpack/environment.js new file mode 100644 index 0000000..d021177 --- /dev/null +++ b/webpack/environment.js @@ -0,0 +1,6 @@ +module.exports = { + I18N_HASH: 'generated_hash', + SERVER_API_URL: '', + __VERSION__: process.env.hasOwnProperty('APP_VERSION') ? process.env.APP_VERSION : 'DEV', + __DEBUG_INFO_ENABLED__: false, +}; diff --git a/webpack/logo-jhipster.png b/webpack/logo-jhipster.png new file mode 100644 index 0000000..e301aa9 Binary files /dev/null and b/webpack/logo-jhipster.png differ diff --git a/webpack/proxy.conf.js b/webpack/proxy.conf.js new file mode 100644 index 0000000..a123e6c --- /dev/null +++ b/webpack/proxy.conf.js @@ -0,0 +1,14 @@ +function setupProxy({ tls }) { + const serverResources = ['/login/saml2/sso', '/saml2', '/api', '/services', '/management', '/v3/api-docs', '/h2-console', '/health']; + const conf = [ + { + context: serverResources, + target: `http${tls ? 's' : ''}://localhost:8081`, + secure: false, + changeOrigin: tls, + }, + ]; + return conf; +} + +module.exports = setupProxy; diff --git a/webpack/webpack.custom.js b/webpack/webpack.custom.js new file mode 100644 index 0000000..cb25c47 --- /dev/null +++ b/webpack/webpack.custom.js @@ -0,0 +1,140 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const { hashElement } = require('folder-hash'); +const MergeJsonWebpackPlugin = require('merge-jsons-webpack-plugin'); +const BrowserSyncPlugin = require('browser-sync-webpack-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const WebpackNotifierPlugin = require('webpack-notifier'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const ESLintPlugin = require('eslint-webpack-plugin'); + +const environment = require('./environment'); +const proxyConfig = require('./proxy.conf'); + +module.exports = async (config, options, targetOptions) => { + const languagesHash = await hashElement(path.resolve(__dirname, '../src/main/webapp/i18n'), { + algo: 'md5', + encoding: 'hex', + files: { include: ['*.json'] }, + }); + + // PLUGINS + if (config.mode === 'development') { + config.plugins.push( + new ESLintPlugin({ + baseConfig: { + parserOptions: { + project: ['../tsconfig.app.json'], + }, + }, + }), + new WebpackNotifierPlugin({ + title: 'Resilient', + contentImage: path.join(__dirname, 'logo-jhipster.png'), + }), + ); + } + + // configuring proxy for back end service + const tls = config.devServer?.server?.type === 'https'; + if (config.devServer) { + config.devServer.proxy = proxyConfig({ tls }); + } + + if (targetOptions.target === 'serve' || config.watch) { + config.plugins.push( + new BrowserSyncPlugin( + { + host: 'localhost', + port: 9000, + https: tls, + proxy: { + target: `http${tls ? 's' : ''}://localhost:${targetOptions.target === 'serve' ? '4200' : '8081'}`, + ws: true, + proxyOptions: { + changeOrigin: false, //pass the Host header to the backend unchanged https://github.com/Browsersync/browser-sync/issues/430 + }, + }, + socket: { + clients: { + heartbeatTimeout: 60000, + }, + }, + /* + ghostMode: { // uncomment this part to disable BrowserSync ghostMode; https://github.com/jhipster/generator-jhipster/issues/11116 + clicks: false, + location: false, + forms: false, + scroll: false, + }, + */ + }, + { + reload: targetOptions.target === 'build', // enabled for build --watch + }, + ), + ); + } + + if (config.mode === 'production') { + config.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + openAnalyzer: false, + // Webpack statistics in temporary folder + reportFilename: '../../stats.html', + }), + ); + } + + const patterns = [ + { + // https://github.com/swagger-api/swagger-ui/blob/v4.6.1/swagger-ui-dist-package/README.md + context: require('swagger-ui-dist').getAbsoluteFSPath(), + from: '*.{js,css,html,png}', + to: 'swagger-ui/', + globOptions: { ignore: ['**/index.html'] }, + }, + { + from: path.join(path.dirname(require.resolve('axios/package.json')), 'dist/axios.min.js'), + to: 'swagger-ui/', + }, + { from: './src/main/webapp/swagger-ui/', to: 'swagger-ui/' }, + // jhipster-needle-add-assets-to-webpack - JHipster will add/remove third-party resources in this array + ]; + + if (patterns.length > 0) { + config.plugins.push(new CopyWebpackPlugin({ patterns })); + } + + config.plugins.push( + new webpack.DefinePlugin({ + I18N_HASH: JSON.stringify(languagesHash.hash), + // APP_VERSION is passed as an environment variable from the Gradle / Maven build tasks. + __VERSION__: JSON.stringify(environment.__VERSION__), + __DEBUG_INFO_ENABLED__: environment.__DEBUG_INFO_ENABLED__ || config.mode === 'development', + // The root URL for API calls, ending with a '/' - for example: `"https://www.jhipster.tech:8081/myservice/"`. + // If this URL is left empty (""), then it will be relative to the current context. + // If you use an API server, in `prod` mode, you will need to enable CORS + // (see the `jhipster.cors` common JHipster property in the `application-*.yml` configurations) + SERVER_API_URL: JSON.stringify(environment.SERVER_API_URL), + }), + new MergeJsonWebpackPlugin({ + output: { + groupBy: [ + { pattern: './src/main/webapp/i18n/pt-pt/*.json', fileName: './i18n/pt-pt.json' }, + { pattern: './src/main/webapp/i18n/en/*.json', fileName: './i18n/en.json' }, + // jhipster-needle-i18n-language-webpack - JHipster will add/remove languages in this array + ], + }, + }), + ); + + config = merge( + config, + // jhipster-needle-add-webpack-config - JHipster will add custom config + ); + + return config; +};